1use std::{fmt::Display, str::FromStr};
2
3use geo_types::{LineString, Point, Polygon};
4
5use crate::{
6 ParseError,
7 constants::*,
8 error::OutOfBoundsError,
9 grid::{GRID, coords_to_grid, grid_to_coords},
10 grid_reference::GridReference,
11 resolution::Resolution,
12};
13
14const OFFSET_EAST: u32 = _500KM * 2;
16const OFFSET_NORTH: u32 = _500KM;
17
18const BOUNDS_EAST: u32 = (_500KM * 5) - OFFSET_EAST;
20const BOUNDS_NORTH: u32 = (_500KM * 5) - OFFSET_NORTH;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct OSGB {
32 point: crate::grid_reference::GridReference,
33 square_500k_east: u32,
34 square_500k_north: u32,
35}
36
37impl OSGB {
38 pub fn new(
57 eastings: u32,
58 northings: u32,
59 resolution: Resolution,
60 ) -> Result<Self, OutOfBoundsError> {
61 match (eastings >= BOUNDS_EAST, northings >= BOUNDS_NORTH) {
62 (true, true) => Err(OutOfBoundsError::Both),
63 (true, false) => Err(OutOfBoundsError::Eastings),
64 (false, true) => Err(OutOfBoundsError::Northings),
65 (false, false) => {
66 let square_500k_east = (eastings + OFFSET_EAST) / _500KM;
67 let square_500k_north = (northings + OFFSET_NORTH) / _500KM;
68 let eastings = eastings % _500KM;
69 let northings = northings % _500KM;
70
71 Ok(Self {
72 point: GridReference::new(eastings, northings, resolution)?,
73 square_500k_east,
74 square_500k_north,
75 })
76 }
77 }
78 }
79
80 pub fn eastings(&self) -> u32 {
81 let east_500k = (self.square_500k_east * _500KM) - OFFSET_EAST;
82
83 east_500k + self.point.eastings()
84 }
85
86 pub fn northings(&self) -> u32 {
87 let north_500k = (self.square_500k_north * _500KM) - OFFSET_NORTH;
88
89 north_500k + self.point.northings()
90 }
91
92 pub fn resolution(&self) -> Resolution {
93 self.point.resolution()
94 }
95
96 pub fn recalculate(&self, resolution: Resolution) -> Self {
108 if resolution.metres() <= self.point.resolution().metres() {
109 self.clone()
110 } else {
111 Self {
112 square_500k_east: self.square_500k_east,
113 square_500k_north: self.square_500k_north,
114 point: self.point.recalculate(resolution),
115 }
116 }
117 }
118
119 pub fn south_west(&self) -> Point {
131 Point::new(self.eastings() as f64, self.northings() as f64)
132 }
133
134 pub fn north_west(&self) -> Point {
146 Point::new(
147 self.eastings() as f64,
148 (self.northings() + self.point.resolution().metres()) as f64,
149 )
150 }
151
152 pub fn north_east(&self) -> Point {
164 Point::new(
165 (self.eastings() + self.point.resolution().metres()) as f64,
166 (self.northings() + self.point.resolution().metres()) as f64,
167 )
168 }
169
170 pub fn south_east(&self) -> Point {
182 Point::new(
183 (self.eastings() + self.point.resolution().metres()) as f64,
184 self.northings() as f64,
185 )
186 }
187
188 pub fn centre(&self) -> Point {
200 Point::new(
201 self.eastings() as f64 + (self.point.resolution().metres() as f64 / 2.0),
202 self.northings() as f64 + (self.point.resolution().metres() as f64 / 2.0),
203 )
204 }
205
206 pub fn perimeter(&self) -> Polygon {
231 Polygon::new(
232 LineString::from(vec![
233 self.south_west(),
234 self.north_west(),
235 self.north_east(),
236 self.south_east(),
237 ]),
238 vec![],
239 )
240 }
241}
242
243impl FromStr for OSGB {
244 type Err = ParseError;
245
246 fn from_str(s: &str) -> Result<Self, Self::Err> {
247 match s.chars().next() {
248 Some(c) => {
249 let (east, north) = grid_to_coords(&c, &GRID)?;
250 match (
251 (east as u32 * _500KM) < OFFSET_EAST,
252 (north as u32 * _500KM) < OFFSET_NORTH,
253 ) {
254 (true, true) => Err(ParseError::OutOfBounds(OutOfBoundsError::Both)),
255 (true, false) => Err(ParseError::OutOfBounds(OutOfBoundsError::Eastings)),
256 (false, true) => Err(ParseError::OutOfBounds(OutOfBoundsError::Northings)),
257 (false, false) => Ok(Self {
258 point: GridReference::from_string_rep(&s[1..])?,
259 square_500k_east: east as u32,
260 square_500k_north: north as u32,
261 }),
262 }
263 }
264 None => Err(ParseError::InvalidString("Found empty string".to_string())),
265 }
266 }
267}
268
269impl Display for OSGB {
270 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
271 let square = coords_to_grid(
272 self.square_500k_east as usize,
273 self.square_500k_north as usize,
274 &GRID,
275 );
276
277 write!(f, "{}{}", square, self.point.to_string_rep())
278 }
279}
280
281#[cfg(feature = "serde")]
282mod serde {
283 use crate::OSGB;
284 use serde::{de, ser};
285 use std::fmt;
286
287 impl ser::Serialize for OSGB {
288 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
289 where
290 S: ser::Serializer,
291 {
292 serializer.serialize_str(&self.to_string())
293 }
294 }
295
296 struct OSGBVisitor;
297
298 impl<'de> de::Visitor<'de> for OSGBVisitor {
299 type Value = OSGB;
300
301 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
302 formatter.write_str("a formatted grid ref string")
303 }
304
305 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
306 where
307 E: de::Error,
308 {
309 value.parse().map_err(E::custom)
310 }
311 }
312
313 impl<'de> de::Deserialize<'de> for OSGB {
314 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
315 where
316 D: de::Deserializer<'de>,
317 {
318 deserializer.deserialize_str(OSGBVisitor)
319 }
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn points_print() {
329 let tests = vec![
330 (Resolution::_100km, "TL"),
331 #[cfg(feature = "quadrants")]
332 (Resolution::_50km, "TLSW"),
333 (Resolution::_10km, "TL03"),
334 #[cfg(feature = "quadrants")]
335 (Resolution::_5km, "TL03NW"),
336 #[cfg(feature = "tetrads")]
337 (Resolution::_2km, "TL03P"),
338 (Resolution::_1km, "TL0438"),
339 #[cfg(feature = "quadrants")]
340 (Resolution::_500m, "TL0438SE"),
341 (Resolution::_100m, "TL048380"),
342 #[cfg(feature = "quadrants")]
343 (Resolution::_50m, "TL048380SE"),
344 (Resolution::_10m, "TL04863802"),
345 #[cfg(feature = "quadrants")]
346 (Resolution::_5m, "TL04863802SE"),
347 (Resolution::_1m, "TL0486638023"),
348 ];
349 for test in tests {
350 let point = OSGB::new(504866, 238023, test.0).unwrap();
351
352 assert_eq!(&point.to_string(), test.1);
353 }
354 }
355
356 #[test]
357 fn strings_parse() {
358 let tests = vec![
359 ("TL", (Resolution::_100km, 500000, 200000)),
360 #[cfg(feature = "quadrants")]
361 ("TLSW", (Resolution::_50km, 500000, 200000)),
362 ("TL03", (Resolution::_10km, 500000, 230000)),
363 #[cfg(feature = "quadrants")]
364 ("TL03NW", (Resolution::_5km, 500000, 235000)),
365 #[cfg(feature = "tetrads")]
366 ("TL03P", (Resolution::_2km, 504000, 238000)),
367 ("TL0438", (Resolution::_1km, 504000, 238000)),
368 #[cfg(feature = "quadrants")]
369 ("TL0438SE", (Resolution::_500m, 504500, 238000)),
370 ("TL048380", (Resolution::_100m, 504800, 238000)),
371 #[cfg(feature = "quadrants")]
372 ("TL048380SE", (Resolution::_50m, 504850, 238000)),
373 ("TL04863802", (Resolution::_10m, 504860, 238020)),
374 #[cfg(feature = "quadrants")]
375 ("TL04863802SE", (Resolution::_5m, 504865, 238020)),
376 ("TL0486638023", (Resolution::_1m, 504866, 238023)),
377 ];
378
379 for test in tests {
380 println!("Testing string: {}", test.0);
381 let point = OSGB::from_str(test.0).unwrap();
382
383 assert_eq!(point.resolution(), test.1.0);
384 assert_eq!(point.eastings(), test.1.1);
385 assert_eq!(point.northings(), test.1.2);
386 }
387 }
388
389 #[test]
390 fn points_recalculate() {
391 let point = OSGB::new(504866, 238023, Resolution::_1m).unwrap();
392 let tests = vec![
393 (Resolution::_100km, (500000, 200000)),
394 #[cfg(feature = "quadrants")]
395 (Resolution::_50km, (500000, 200000)),
396 (Resolution::_10km, (500000, 230000)),
397 #[cfg(feature = "quadrants")]
398 (Resolution::_5km, (500000, 235000)),
399 #[cfg(feature = "tetrads")]
400 (Resolution::_2km, (504000, 238000)),
401 (Resolution::_1km, (504000, 238000)),
402 #[cfg(feature = "quadrants")]
403 (Resolution::_500m, (504500, 238000)),
404 (Resolution::_100m, (504800, 238000)),
405 #[cfg(feature = "quadrants")]
406 (Resolution::_50m, (504850, 238000)),
407 (Resolution::_10m, (504860, 238020)),
408 #[cfg(feature = "quadrants")]
409 (Resolution::_5m, (504865, 238020)),
410 (Resolution::_1m, (504866, 238023)),
411 ];
412
413 for test in tests {
414 let new = point.recalculate(test.0);
415 assert_eq!((new.eastings(), new.northings()), test.1);
416 }
417 }
418
419 #[test]
420 fn recalculate_does_not_increase_resolution() {
421 let tests = vec![
422 Resolution::_100km,
423 #[cfg(feature = "quadrants")]
424 Resolution::_50km,
425 Resolution::_10km,
426 #[cfg(feature = "quadrants")]
427 Resolution::_5km,
428 #[cfg(feature = "tetrads")]
429 Resolution::_2km,
430 Resolution::_1km,
431 #[cfg(feature = "quadrants")]
432 Resolution::_500m,
433 Resolution::_100m,
434 #[cfg(feature = "quadrants")]
435 Resolution::_50m,
436 Resolution::_10m,
437 #[cfg(feature = "quadrants")]
438 Resolution::_5m,
439 ];
440
441 for test in tests {
442 let point = OSGB::new(504866, 238023, test).unwrap();
443
444 assert_eq!(
445 point.recalculate(Resolution::_1m).resolution(),
446 point.resolution()
447 );
448 }
449 }
450
451 #[test]
452 #[cfg(not(feature = "tetrads"))]
453 fn tetrads_are_rejected_when_not_enabled() {
454 cfg_select! {
455 feature = "quadrants" => {
456 assert_eq!(
457 OSGB::from_str("TL03P"),
458 Err(ParseError::InvalidString("P is not a valid quadrant.".to_string()))
459 );
460 }
461 _ => {
462 assert_eq!(
463 OSGB::from_str("TL03P"),
464 Err(ParseError::InvalidString("Extra characters found after digits.".to_string()))
465 );
466 }
467 }
468 }
469
470 #[test]
471 #[cfg(not(feature = "quadrants"))]
472 fn quadrants_are_rejected_when_not_enabled() {
473 cfg_select! {
474 feature = "tetrads" => {
475 assert_eq!(
476 OSGB::from_str("TL03SW"),
477 Err(ParseError::InvalidString("SW is not a valid tetrad.".to_string()))
478 );
479 }
480 _ => {
481 assert_eq!(
482 OSGB::from_str("TL03SW"),
483 Err(ParseError::InvalidString("Extra characters found after digits.".to_string()))
484 );
485 }
486 }
487 }
488}