diff --git a/dsky-utils/src/lib.rs b/dsky-utils/src/lib.rs index 3aa5b60..743c437 100644 --- a/dsky-utils/src/lib.rs +++ b/dsky-utils/src/lib.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(feature = "std"), no_std)] -use substrate_fixed::types::I10F22; +use substrate_fixed::{types::{I10F22, I42F22}, traits::FromFixed}; +// Set of traits, required for Coord struct, used in maps pallet pub trait IntDiv { fn integer_division_u16(self, rhs: RHS) -> u16; @@ -9,6 +10,7 @@ pub trait IntDiv { pub trait Signed { fn abs(self) -> Self; + fn signum(self) -> Self; } pub trait FromRaw { @@ -19,6 +21,22 @@ pub trait CastToType { fn to_u32_with_frac_part(self, cell_size: u32, max_digits_in_frac_part: u8) -> u32; } +// TODO consider naming of two traits, as they are used in pair +pub trait ToBigCoord { + type Output; + fn try_into(self) -> Self::Output; +} + +pub trait FromBigCoord { + type Output; + fn try_from(self) -> Self::Output; +} + +pub trait GetEpsilon { + fn get_epsilon() -> Self; +} + +// Here comes the implementations // Want to change Coord type => impl trait for it here impl IntDiv for I10F22 { fn integer_division_u16(self, rhs: I10F22) -> u16 { @@ -40,6 +58,10 @@ impl Signed for I10F22 { fn abs(self) -> Self { self.abs() } + /// returns sign (-1, 1, 0) + fn signum(self) -> Self { + self.signum() + } } impl CastToType for I10F22 { @@ -63,3 +85,24 @@ impl CastToType for I10F22 { integer_part + frac_part } } + +impl ToBigCoord for I10F22 { + type Output = I42F22; + fn try_into(self) -> Self::Output { + self.into() + } +} + +// TODO handle possible errors through checked_from_fixed() +impl FromBigCoord for I42F22 { + type Output = I10F22; + fn try_from(self) -> Self::Output { + I10F22::from_fixed(self) + } +} + +impl GetEpsilon for I42F22 { + fn get_epsilon() -> I42F22 { + I42F22::from_num(0.00001f64) + } +} diff --git a/pallets/ds-maps/src/default_weight.rs b/pallets/ds-maps/src/default_weight.rs index 6354bd7..6aca0f3 100644 --- a/pallets/ds-maps/src/default_weight.rs +++ b/pallets/ds-maps/src/default_weight.rs @@ -16,5 +16,7 @@ impl crate::WeightInfo for () { fn change_area_type() -> Weight { 100_000_u64.saturating_add(DbWeight::get().writes(1)) } - + fn route_add() -> Weight { + 100_000_u64.saturating_add(DbWeight::get().writes(1)) + } } diff --git a/pallets/ds-maps/src/lib.rs b/pallets/ds-maps/src/lib.rs index 3387b10..20a2665 100644 --- a/pallets/ds-maps/src/lib.rs +++ b/pallets/ds-maps/src/lib.rs @@ -7,7 +7,7 @@ use frame_support::{ codec::{Decode, Encode}, storage::StorageDoubleMap, dispatch::fmt::Debug, - sp_runtime::sp_std::{ops::{Sub, Div}, vec::Vec}, + sp_runtime::sp_std::{ops::{Sub, Div, Mul, Add}, vec::Vec}, decl_error, decl_event, decl_module, decl_storage, dispatch, ensure, weights::Weight, Parameter, @@ -17,10 +17,12 @@ use frame_support::{ use sp_std::{ str::FromStr, marker::PhantomData, - vec, + vec, + mem::swap, + cmp::{max, min} }; -use dsky_utils::{CastToType, FromRaw, IntDiv, Signed}; +use dsky_utils::{CastToType, FromRaw, IntDiv, Signed, ToBigCoord, FromBigCoord, GetEpsilon}; use frame_system::ensure_signed; use pallet_ds_accounts as accounts; use accounts::REGISTRAR_ROLE; @@ -185,10 +187,16 @@ pub struct Point3D { alt: Coord, } -impl Point3D { +impl< + Coord: PartialOrd + Sub + Signed + IntDiv + > Point3D { pub fn new(lat: Coord, lon: Coord, alt: Coord) -> Self { Point3D{lat, lon, alt} } + + pub fn project(self) -> Point2D { + Point2D::new(self.lat, self.lon) + } } #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] @@ -207,16 +215,355 @@ impl< /// Gets rect 2D projection from a box pub fn projection_on_plane(self) -> Rect2D { - let south_west = - Point2D::new(self.south_west.lat, - self.south_west.lon); - let north_east = - Point2D::new(self.north_east.lat, - self.north_east.lon); + let south_west = self.south_west.project(); + let north_east = self.north_east.project(); Rect2D::new(south_west, north_east) } } +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Clone, Default, Debug, PartialEq, Eq)] +pub struct Waypoint { + pub location: Point3D, + pub arrival: Moment, +} + +impl Waypoint { + pub fn new(location: Point3D, arrival: Moment) -> Self { + Waypoint{location, arrival} + } +} + +// Actually, this is line section, so we need limits +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Encode, Decode, Clone, Copy, Default, Debug, PartialEq, Eq)] +pub struct Line { + // pub a: Coord, + // pub b: Coord, + // pub c: Coord, + pub start_point: Point2D, + pub end_point: Point2D, + _phantom: PhantomData +} + +impl< + Coord: FromStr + Default + ToBigCoord + Ord + Signed + IntDiv + Mul + Sub + Add + Div + Copy, + BigCoord: Mul + Sub + FromBigCoord + Copy + PartialOrd + GetEpsilon + > Line { + pub fn new(point_0: Point2D, point_1: Point2D) -> Self { + // Form coefficients (y1 - y2)x + (x2 - x1)y + (x1y2 - x2y1) = 0 + // TODO (n>2) in a cycle + // let a = (point_0.lat.try_into() - point_1.lat.try_into()).try_from(); + // let b = (point_1.lon.try_into() - point_0.lon.try_into()).try_from(); + // let c = ( + // (point_0.lon.try_into() * point_1.lat.try_into()) - + // (point_1.lon.try_into() * point_0.lat.try_into()) + // ).try_from(); + Line { + // a: a, + // b: b, + // c: c, + start_point: point_0, + end_point: point_1, + _phantom: PhantomData + } + } + fn coord_from_str (s: &str) -> Coord { + match Coord::from_str(s) { + Ok(v) => v, + Err(_) => Default::default(), + } + } + + // Realisation of Bresenham's line algorithm + pub fn get_route_areas(self, root: RootBox) -> Vec { + let start_area = root.detect_intersected_area(self.start_point); + let end_area = root.detect_intersected_area(self.end_point); + // In case everything is in one area + if start_area == end_area {return vec![start_area]} + let mut output = Vec::new(); + let delta = root.delta; + // Simplification, it's still lat/lon + let mut dx = self.end_point.lat - self.start_point.lat; + let mut dy = self.end_point.lon - self.start_point.lon; + let incx = dx.signum() * delta; + let incy = dy.signum() * delta; + dx = dx.abs(); + dy = dy.abs(); + let pdx: Coord; + let pdy: Coord; + let es: Coord; + let el: Coord; + let zero: Coord = Line::coord_from_str("0"); + if dx > dy { + pdx = incx; pdy = zero; + es = dy; el = dx; + } else { + pdx = zero; pdy = incy; + es = dx; el = dy; + } + + let mut current_point = self.start_point; + output.push(root.detect_intersected_area(current_point)); + let mut count: Coord = zero; + let mut err = el / Line::coord_from_str("2"); + while count < el { + count = count + delta; + err = err - es; + if err < zero { + err = err + el; + current_point.lat = current_point.lat + incx; + current_point.lon = current_point.lon + incy; + } else { + current_point.lat = current_point.lat + pdx; + current_point.lon = current_point.lon + pdy; + } + output.push(root.detect_intersected_area(current_point)); + } + output + } + + // Basically we split rect to 4 lines(maybe 2 is enough?), and check each one for intersection + pub fn intersects_rect(&self, rect: Rect2D) -> bool { + // true + let south_east = Point2D::new(rect.north_east.lat, rect.south_west.lon); + let north_west = Point2D::new(rect.south_west.lat, rect.north_east.lon); + + let west_line = Line::new(rect.south_west, north_west); + let north_line = Line::new(north_west, rect.north_east); + let east_line = Line::new(rect.north_east, south_east); + let south_line = Line::new(south_east, rect.south_west); + + let rect_lines = vec![west_line, north_line, east_line, south_line]; + + for line in rect_lines.iter() { + if self.is_lines_cross(*line) { return true; } + } + false + } + // This method allows to proof intersection, but can't find intersection points + pub fn is_lines_cross(&self, line: Line) -> bool { + // Checking if the segments are collinear + Line::projection_intersect(self.start_point.lat, self.end_point.lat, line.start_point.lat, line.end_point.lat) && + Line::projection_intersect(self.start_point.lon, self.end_point.lon, line.start_point.lon, line.end_point.lon) && + // Check, that points of AB lies on different sides of CD, and vice versa + // TODO basically we need only signs and zero-condition, consider refactor + ((Line::area(self.start_point, self.end_point, line.start_point) * + Line::area(self.start_point, self.end_point, line.end_point)) <= BigCoord::get_epsilon()) && + ((Line::area(line.start_point, line.end_point, self.start_point) * + Line::area(line.start_point, line.end_point, self.end_point)) <= BigCoord::get_epsilon()) + } + + // Checks for projections not to intersect + fn projection_intersect(_a: Coord, _b: Coord, _c: Coord, _d: Coord) -> bool { + let (mut a, mut b, mut c, mut d) = (_a, _b, _c, _d); + if a > b { swap(&mut a, &mut b) } + if c > d { swap(&mut c, &mut d) } + + max(a,c) <= min(b,d) + } + + // Yea, this fn hurts + // Signed area of a triangle, oriented clockwise (same as vector multiplication) + fn area(a: Point2D, b: Point2D, c: Point2D) -> BigCoord { + (b.lat.try_into() - a.lat.try_into()) * (c.lon.try_into() - a.lon.try_into()) - + (b.lon.try_into() - a.lon.try_into()) * (c.lat.try_into() - a.lat.try_into()) + } +} + +#[cfg(test)] +mod line_tests { + use super::*; + use crate::tests::{construct_custom_box, construct_testing_rect, coord, Coord}; + // TODO draw rect in ascii as an illustration + // +---+ + // | | + // +---+ + // In this section we try test calculations for intersecting areas with line + mod route_area_tests { + use super::*; + + #[test] + fn get_route_areas_in_square() { + let rect = construct_custom_box("0", "0", "4", "4"); + let root = RootBox::new(1, rect, coord("1")); + let first_point = Point2D::new(coord("0.5"), + coord("0.5")); + let second_point = Point2D::new(coord("2.5"), + coord("2.5")); + let line = Line::new(first_point, second_point); + let areas = line.get_route_areas(root); + assert_eq!(areas, vec![1, 6, 11]); + } + + #[test] + fn get_route_areas_in_line() { + let rect = construct_custom_box("0", "0", "4", "4"); + let root = RootBox::new(1, rect, coord("1")); + let first_point = Point2D::new(coord("0.1"), + coord("0.1")); + let second_point = Point2D::new(coord("3.1"), + coord("0.2")); + let line = Line::new(first_point, second_point); + let areas = line.get_route_areas(root); + assert_eq!(areas, vec![1, 2, 3, 4]); + } + + #[test] + fn get_route_areas_in_frac_delta() { + let rect = construct_custom_box("0", "0", "4", "4"); + let root = RootBox::new(1, rect, coord("0.1")); + let first_point = Point2D::new(coord("0.01"), + coord("0.01")); + let second_point = Point2D::new(coord("0.31"), + coord("0.02")); + let line = Line::new(first_point, second_point); + let areas = line.get_route_areas(root); + assert_eq!(areas, vec![1, 2, 3, 4, 5]); + } + + #[test] + fn get_route_single_area() { + let rect = construct_custom_box("0", "0", "4", "4"); + let root = RootBox::new(1, rect, coord("1")); + let first_point = Point2D::new(coord("0.1"), + coord("0.1")); + let second_point = Point2D::new(coord("0.3"), + coord("0.2")); + let line = Line::new(first_point, second_point); + let areas = line.get_route_areas(root); + assert_eq!(areas, vec![1]); + } + } + + // In this section we try crossing different lines + mod crossing_line_tests { + use super::*; + + #[test] + fn lines_cross() { + // I'm not sure, why do i have to specify variable type in each fn + let a_first_point: Point2D = Point2D::new(coord("0.1"), + coord("0.1")); + let a_second_point = Point2D::new(coord("1"), + coord("1")); + let b_first_point = Point2D::new(coord("0.1"), + coord("1")); + let b_second_point = Point2D::new(coord("1"), + coord("0.2")); + let a = Line::new(a_first_point, a_second_point); + let b = Line::new(b_first_point, b_second_point); + assert!(a.is_lines_cross(b)); + } + + #[test] + fn long_lines_cross() { + let a_first_point: Point2D = Point2D::new(coord("10"), + coord("0.1")); + let a_second_point = Point2D::new(coord("10"), + coord("10")); + let b_first_point = Point2D::new(coord("5"), + coord("5")); + let b_second_point = Point2D::new(coord("15"), + coord("5")); + let a = Line::new(a_first_point, a_second_point); + let b = Line::new(b_first_point, b_second_point); + assert!(a.is_lines_cross(b)); + } + + #[test] + fn lines_do_not_cross() { + let a_first_point: Point2D = Point2D::new(coord("0.1"), + coord("0.1")); + let a_second_point = Point2D::new(coord("1"), + coord("1")); + let b_first_point = Point2D::new(coord("1.3"), + coord("1.3")); + let b_second_point = Point2D::new(coord("2"), + coord("4")); + let a = Line::new(a_first_point, a_second_point); + let b = Line::new(b_first_point, b_second_point); + assert!(!a.is_lines_cross(b)); + } + + #[test] + fn lines_cross_at_high_angle() { + let a_first_point: Point2D = Point2D::new(coord("10"), + coord("0.1")); + let a_second_point = Point2D::new(coord("10"), + coord("10")); + let b_first_point = Point2D::new(coord("9.8"), + coord("0.1")); + let b_second_point = Point2D::new(coord("10.1"), + coord("10")); + let a = Line::new(a_first_point, a_second_point); + let b = Line::new(b_first_point, b_second_point); + assert!(a.is_lines_cross(b)); + } + + #[test] + fn line_through_rect() { + // It is becoming kinda long, but there's a lot of coords in use + let rect_first_point: Point2D = Point2D::new(coord("1"), + coord("1")); + let rect_second_point = Point2D::new(coord("2"), + coord("15")); + let first_point = Point2D::new(coord("0.1"), + coord("3")); + let second_point = Point2D::new(coord("4"), + coord("3")); + let rect = Rect2D::new(rect_first_point, rect_second_point); + let line = Line::new(first_point, second_point); + assert!(line.intersects_rect(rect)); + } + + #[test] + fn line_partially_inside_rect() { + let rect_first_point: Point2D = Point2D::new(coord("1"), + coord("1")); + let rect_second_point = Point2D::new(coord("2"), + coord("15")); + let first_point = Point2D::new(coord("0.1"), + coord("3")); + let second_point = Point2D::new(coord("1.5"), + coord("3")); + let rect = Rect2D::new(rect_first_point, rect_second_point); + let line = Line::new(first_point, second_point); + assert!(line.intersects_rect(rect)); + } + + + #[test] + fn line_outside_rect() { + let rect_first_point: Point2D = Point2D::new(coord("1"), + coord("1")); + let rect_second_point = Point2D::new(coord("2"), + coord("15")); + let first_point = Point2D::new(coord("3"), + coord("3")); + let second_point = Point2D::new(coord("4"), + coord("6")); + let rect = Rect2D::new(rect_first_point, rect_second_point); + let line = Line::new(first_point, second_point); + assert!(!line.intersects_rect(rect)); + } + + #[test] + fn line_rect_gps_coords() { + let first_point = Point2D::new(coord("55.392"), + coord("37.386")); + let second_point = Point2D::new(coord("55.396"), + coord("37.386")); + let rect = construct_testing_rect(); + let intersecting_line = Line::new(first_point, second_point); + assert!(intersecting_line.intersects_rect(rect)); + let third_point = Point2D::new(coord("55.393"), coord("37.386")); + let outside_line = Line::new(first_point, third_point); + assert!(!outside_line.intersects_rect(rect)); + } + } +} + #[cfg_attr(feature = "std", derive(Serialize, Deserialize))] #[derive(Encode, Decode, Default, Clone, Copy, Debug)] pub struct RootBox { @@ -226,7 +573,7 @@ pub struct RootBox { } impl< - Coord: PartialOrd + Sub + Signed + IntDiv + Div + Copy + Coord: PartialOrd + Sub + Signed + IntDiv + Mul + Div + Copy, > RootBox { pub fn new(id: RootId, bounding_box: Box3D, delta: Coord) -> Self { RootBox{id, bounding_box, delta} @@ -967,20 +1314,36 @@ pub trait Trait: accounts::Trait { + Parameter + Copy + PartialOrd + + Ord + PartialEq + FromStr - + IntDiv + Signed + + Sub + + Div + + Mul + + Add + // Traits from dsky-utils + + IntDiv + FromRaw + CastToType - + Sub - + Div; - + + ToBigCoord; + + // Required for global calculations, where Coord is not enough. Not for common usage. + type BigCoord: Sub + + Div + + Mul + + Add + + Default + + PartialOrd + + Copy + + FromBigCoord + + GetEpsilon; + type RawCoord: Default + Parameter + Into + Copy; - + /// This allows us to have a top border for zones type MaxBuildingsInArea: Get; @@ -994,6 +1357,7 @@ pub trait WeightInfo { fn root_remove() -> Weight; fn zone_remove() -> Weight; fn change_area_type() -> Weight; + fn route_add() -> Weight; } decl_storage! { @@ -1026,6 +1390,8 @@ decl_event!( pub enum Event where AccountId = ::AccountId, + Moment = ::Moment, + Coord = ::Coord, { // Event documentation should end with an array that provides descriptive names for event parameters. /// New root box has been created [box number, who] @@ -1038,6 +1404,8 @@ decl_event!( RootRemoved(RootId, AccountId), /// Zone was removed from storage ZoneRemoved(ZoneId, AccountId), + /// New route was submitted [start, destination, start, arrival, rootId, who] + RouteAdded(Point3D, Point3D, Moment, Moment, RootId, AccountId), } ); @@ -1075,6 +1443,12 @@ decl_error! { ZoneDoesntFit, /// Zone you are trying to access is not in storage ZoneDoesntExist, + /// Wrong time bounds are supplied + WrongTimeSupplied, + /// Route contain 1 or more wpoints, which lie outside of root + RouteDoesNotFitToRoot, + /// Route intersect 1 or more zones + RouteIntersectRedZone, // Add additional errors below } } @@ -1385,6 +1759,75 @@ decl_module! { Self::deposit_event(RawEvent::AreaTypeChanged(area_type, area_id, root_id, who)); Ok(()) } + + /// Creates new route for UAV + #[weight = ::WeightInfo::route_add()] + pub fn route_add(origin, + waypoints: Vec::Moment>>, + root_id: RootId) -> dispatch::DispatchResult { + let who = ensure_signed(origin)?; + // TODO consider role for route addition + ensure!(>::account_is(&who, REGISTRAR_ROLE.into()), Error::::NotAuthorized); + ensure!(RootBoxes::::contains_key(root_id), Error::::RootDoesNotExist); + ensure!(waypoints.len() >= 2, Error::::InvalidData); + let start_waypoint = &waypoints.first().unwrap(); + let end_waypoint = &waypoints.last().unwrap(); + // Getting all time bounds + let start_time = start_waypoint.arrival; + let arrival_time = end_waypoint.arrival; + let current_timestamp = >::get(); + // TODO (n>2) wp[n].arrival < wp[n + 1].arrival + ensure!((arrival_time > current_timestamp) && (arrival_time > start_time), Error::::WrongTimeSupplied); + + let root = RootBoxes::::get(root_id); + let start_area = root.detect_intersected_area(start_waypoint.location.project()); + let end_area = root.detect_intersected_area(end_waypoint.location.project()); + // TODO (n>2) each wp[n].location shall be inside one Root + ensure!((start_area != 0) && (end_area != 0), Error::::RouteDoesNotFitToRoot); + // TODO (n>2) vec! in here + let route_line = Line::new(start_waypoint.location.project(), end_waypoint.location.project()); + // We receive all areas, containing list of zones which could be intersected + let route_areas: Vec = route_line.get_route_areas(root); + // Loop through areas, check each existing zone + for area_id in route_areas.iter() { + if AreaData::contains_key(root_id, area_id) { + let mut zone_id = Self::pack_index(root_id, *area_id, 0); + // Loop through zones, maybe add constraint to MaxBuildingsInArea + while RedZones::::contains_key(zone_id) { + // TODO ask about ensure!() usage in cycle + ensure!(!route_line.intersects_rect(RedZones::::get(zone_id).rect), Error::::RouteIntersectRedZone); + zone_id += 1; + } + } + } + Self::deposit_event(RawEvent::RouteAdded( + start_waypoint.location, end_waypoint.location, + start_time, arrival_time, root_id, who + )); + Ok(()) + } + + #[weight = ::WeightInfo::route_add()] + pub fn raw_route_add(origin, + raw_waypoints: [T::RawCoord; 4], + start_time: T::Moment, + arrival_time: T::Moment, + root_id: RootId) -> dispatch::DispatchResult { + let start_location = Point3D::new( + T::Coord::from_raw(raw_waypoints[0].into()), + T::Coord::from_raw(raw_waypoints[1].into()), + Self::coord_from_str("1")); + + let start_waypoint = Waypoint::new(start_location, start_time); + + let arrival_location = Point3D::new( + T::Coord::from_raw(raw_waypoints[2].into()), + T::Coord::from_raw(raw_waypoints[3].into()), + Self::coord_from_str("1")); + + let arrival_waypoint = Waypoint::new(arrival_location, arrival_time); + Module::::route_add(origin, vec![start_waypoint, arrival_waypoint], root_id) + } } } diff --git a/pallets/ds-maps/src/mock.rs b/pallets/ds-maps/src/mock.rs index 82e1ea9..bee2120 100644 --- a/pallets/ds-maps/src/mock.rs +++ b/pallets/ds-maps/src/mock.rs @@ -12,7 +12,7 @@ use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, }; -use substrate_fixed::types::I10F22; +use substrate_fixed::types::{I10F22, I42F22}; use pallet_ds_accounts::ADMIN_ROLE; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; @@ -95,6 +95,9 @@ impl crate::WeightInfo for WeightInfo { fn change_area_type() -> Weight { <() as crate::WeightInfo>::change_area_type() } + fn route_add() -> Weight { + <() as crate::WeightInfo>::route_add() + } } // After researches, consider placing here max grid sizes @@ -107,7 +110,7 @@ impl Trait for Test { type Event = Event; type WeightInfo = (); type Coord = I10F22; - + type BigCoord = I42F22; type RawCoord = i32; type MaxBuildingsInArea = MaxBuildingsInArea; type MaxHeight = MaxHeight; diff --git a/pallets/ds-maps/src/tests.rs b/pallets/ds-maps/src/tests.rs index 6d468d4..2389c51 100644 --- a/pallets/ds-maps/src/tests.rs +++ b/pallets/ds-maps/src/tests.rs @@ -3,6 +3,7 @@ use crate::{ Page, Point3D, Box3D, Point2D, Rect2D, + Waypoint, }; use frame_support::{ assert_noop, assert_ok, @@ -11,7 +12,7 @@ use substrate_fixed::types::I10F22; use sp_std::str::FromStr; // Explanation for all hardcoded values down here -// Root P2(55.92, 37.90) +// Root P2(55.921, 37.901) // +----+-------------------------+----O // |2861| |2915| // | | Area 2861 | | @@ -29,19 +30,19 @@ use sp_std::str::FromStr; // | | 1 | 2 | 3 | | 55 | // delta | | | | | | | // =0.01 +-> O----+----+----+---------------+----+ -// Origin(55.37, 37.37) -// +// Origin(55.371, 37.371) // +// (wp1, wp2) is a testing waypoints // Area 58 (55.400, 37.390) // +-----------------------------------o // | | -// | | -// | | +// | (x) | +// | (wp2) | // | rect2D | // | +--------o(55.396, | // | | | 37.386) | -// | |testing | | -// | | zone | | +// | | (x) | | +// | | (wp1) | | // | | | | // | o--------+ | // | (55.395, | @@ -52,17 +53,22 @@ use sp_std::str::FromStr; // (55.390, 37,380) type Error = super::Error; +// TODO find out how to connect this types w mock pub type Coord = I10F22; +type Moment = u64; // Constants to make tests more readable const ADMIN_ACCOUNT_ID: u64 = 1; const REGISTRAR_1_ACCOUNT_ID: u64 = 2; pub const ROOT_ID: u64 = 0b0001_0101_1010_0001_0000_1110_1001_1001_0001_0101_1101_1000_0000_1110_1100_1110; -// this value, and values in construct_testing_..() were calculated +// Values in construct_testing_..() pre-calculated +// construct_custom_..() same functionality, but custom numbers +// These consts also pre-calculated + const AREA_ID: u16 = 58; const DEFAULT_HEIGHT: u32 = 30; -const DELTA: &str = "0.01"; +pub const DELTA: &str = "0.01"; // shortcut for &str -> Coord pub fn coord(s: &str) -> Coord @@ -89,7 +95,7 @@ pub fn construct_custom_box(sw_lat: &str, sw_lon: &str, ne_lat: &str, ne_lon: &s Box3D::new(south_west, north_east) } -fn construct_testing_rect() -> Rect2D { +pub fn construct_testing_rect() -> Rect2D { let south_west = Point2D::new(coord("55.395"), coord("37.385")); let north_east = Point2D::new(coord("55.396"), @@ -105,6 +111,36 @@ pub fn construct_custom_rect(sw_lat: &str, sw_lon: &str, ne_lat: &str, ne_lon: & Rect2D::new(south_west, north_east) } +pub fn construct_testing_waypoints() -> Vec> { + let start_location = Point3D::new(coord("55.395"), + coord("37.385"), + coord("1")); + let end_location = Point3D::new(coord("55.397"), + coord("37.387"), + coord("1")); + let start_time = 100_u64; + let end_time = 110_u64; + let start_wp = Waypoint::new(start_location, start_time); + let end_wp = Waypoint::new(end_location, end_time); + vec![start_wp, end_wp] +} + +// Assume that alt is const for now. +// TODO (n>2) replace &str in signature to an array [[&str; 4]; n] +pub fn construct_custom_waypoints(start_lat: &str, start_lon: &str, + end_lat: &str, end_lon: &str, + start_time: u64, end_time: u64) -> Vec> { + let start_location = Point3D::new(coord(start_lat), + coord(start_lon), + coord("1")); + let end_location = Point3D::new(coord(end_lat), + coord(end_lon), + coord("1")); + let start_wp = Waypoint::new(start_location, start_time); + let end_wp = Waypoint::new(end_location, end_time); + vec![start_wp, end_wp] +} + #[test] fn it_try_to_add_root_unauthorized() { new_test_ext().execute_with(|| { @@ -739,12 +775,374 @@ fn it_dispatchable_get_root_index() { )); // 55.395 - 232343470 // 37.385 - 156804055 - // TODO add explanation, for why this is true let root_id = DSMapsModule::get_root_index([232343470, 156804055]); assert_eq!(root_id, 1558542996706168526); - // For now, this proof will do. We can see, that by this index we get valid, active root + // Proof, that everything is right: by this index we get active root let root = DSMapsModule::root_box_data(root_id); assert!(root.is_active()); }); } +// Not sure if there's need to try this unauthorized +#[test] +fn it_add_route_by_registrar() { + new_test_ext().execute_with(|| { + assert_ok!( + DSAccountsModule::account_add( + Origin::signed(ADMIN_ACCOUNT_ID), + REGISTRAR_1_ACCOUNT_ID, + super::REGISTRAR_ROLE + )); + assert_ok!( + DSMapsModule::root_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_box(), + coord(DELTA), + )); + let waypoints = construct_testing_waypoints(); + assert_ok!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints, + ROOT_ID, + )); + }); +} + +#[test] +fn it_add_route_wrong_root() { + new_test_ext().execute_with(|| { + assert_ok!( + DSAccountsModule::account_add( + Origin::signed(ADMIN_ACCOUNT_ID), + REGISTRAR_1_ACCOUNT_ID, + super::REGISTRAR_ROLE + )); + assert_ok!( + DSMapsModule::root_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_box(), + coord(DELTA), + )); + let waypoints = construct_testing_waypoints(); + assert_noop!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints, + ROOT_ID + 1, + ), + Error::RootDoesNotExist + ); + }); +} + +#[test] +fn it_add_route_wrong_timestamps() { + new_test_ext().execute_with(|| { + assert_ok!( + DSAccountsModule::account_add( + Origin::signed(ADMIN_ACCOUNT_ID), + REGISTRAR_1_ACCOUNT_ID, + super::REGISTRAR_ROLE + )); + assert_ok!( + DSMapsModule::root_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_box(), + coord(DELTA), + )); + // coords are same as in testing wp + let waypoints = construct_custom_waypoints( + "55.395", "37.385", + "55.397", "37.387", + 120, 100); + assert_noop!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints, + ROOT_ID, + ), + Error::WrongTimeSupplied + ); + }); +} + +#[test] +fn it_add_route_wrong_waypoints() { + new_test_ext().execute_with(|| { + assert_ok!( + DSAccountsModule::account_add( + Origin::signed(ADMIN_ACCOUNT_ID), + REGISTRAR_1_ACCOUNT_ID, + super::REGISTRAR_ROLE + )); + assert_ok!( + DSMapsModule::root_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_box(), + coord(DELTA), + )); + let location = Point3D::new(coord("55.395"), + coord("37.385"), + coord("1")); + let single_waypoint = vec![Waypoint::new(location, 10)]; + assert_noop!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + single_waypoint, + ROOT_ID, + ), + Error::InvalidData + ); + + let waypoints = construct_custom_waypoints( + "55.395", "37.385", + "10", "37", + 100, 120); + assert_noop!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints, + ROOT_ID, + ), + Error::RouteDoesNotFitToRoot + ); + }); +} + +// There is no zones to block the way, so it'll pass +#[test] +fn it_add_route_without_zones() { + new_test_ext().execute_with(|| { + assert_ok!( + DSAccountsModule::account_add( + Origin::signed(ADMIN_ACCOUNT_ID), + REGISTRAR_1_ACCOUNT_ID, + super::REGISTRAR_ROLE + )); + assert_ok!( + DSMapsModule::root_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_box(), + coord(DELTA), + )); + let waypoints = construct_testing_waypoints(); + assert_ok!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints, + ROOT_ID, + )); + }); +} + +// This test contain two zones, one is intersecting route, and another is not +#[test] +fn it_add_route_one_area() { + new_test_ext().execute_with(|| { + assert_ok!( + DSAccountsModule::account_add( + Origin::signed(ADMIN_ACCOUNT_ID), + REGISTRAR_1_ACCOUNT_ID, + super::REGISTRAR_ROLE + )); + assert_ok!( + DSMapsModule::root_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_box(), + coord(DELTA), + )); + let waypoints = construct_testing_waypoints(); + // This one do not block the way, and test will pass + assert_ok!( + DSMapsModule::zone_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_custom_rect("55.391", "37.381", "55.392", "37.382"), + DEFAULT_HEIGHT, + ROOT_ID, + )); + assert_ok!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints.clone(), + ROOT_ID, + )); + // But this one will fail, as it blocks the way + assert_ok!( + DSMapsModule::zone_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_rect(), + DEFAULT_HEIGHT, + ROOT_ID, + )); + assert_noop!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints, + ROOT_ID, + ), Error::RouteIntersectRedZone + ); + }); +} + +#[test] +fn it_add_route_multiple_areas() { + new_test_ext().execute_with(|| { + assert_ok!( + DSAccountsModule::account_add( + Origin::signed(ADMIN_ACCOUNT_ID), + REGISTRAR_1_ACCOUNT_ID, + super::REGISTRAR_ROLE + )); + assert_ok!( + DSMapsModule::root_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_box(), + coord(DELTA), + )); + // Route starts at 1 area, and ends in testing rect + let waypoints = construct_custom_waypoints("55.373", "37.373", "55.396", "37.386", 10, 20); + // This one located far from our route, so no intersection + assert_ok!( + DSMapsModule::zone_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_custom_rect("55.411", "37.372", "55.416", "37.375"), + DEFAULT_HEIGHT, + ROOT_ID, + )); + assert_ok!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints.clone(), + ROOT_ID, + )); + // But this one will + assert_ok!( + DSMapsModule::zone_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_rect(), + DEFAULT_HEIGHT, + ROOT_ID, + )); + assert_noop!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints, + ROOT_ID, + ), Error::RouteIntersectRedZone + ); + }); +} + +// Here we add zone, and then we remove it from storage +#[test] +fn it_add_route_and_remove_zone() { + new_test_ext().execute_with(|| { + assert_ok!( + DSAccountsModule::account_add( + Origin::signed(ADMIN_ACCOUNT_ID), + REGISTRAR_1_ACCOUNT_ID, + super::REGISTRAR_ROLE + ) + ); + assert_ok!( + DSMapsModule::root_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_box(), + coord(DELTA), + ) + ); + let waypoints = construct_custom_waypoints("55.373", "37.373", "55.396", "37.386", 10, 20); + assert_ok!( + DSMapsModule::zone_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_rect(), + DEFAULT_HEIGHT, + ROOT_ID, + ) + ); + // Can't add it, zone is blocking the way + assert_noop!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints.clone(), + ROOT_ID, + ), + Error::RouteIntersectRedZone + ); + let zone_index = DSMapsModule::pack_index(ROOT_ID, AREA_ID, 0); + // Now we remove it, clearing the path + assert_ok!( + DSMapsModule::zone_remove( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + zone_index, + ) + ); + assert_ok!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints, + ROOT_ID, + ) + ); + }); +} + +#[test] +fn it_add_lots_of_zones() { + new_test_ext().execute_with(|| { + assert_ok!( + DSAccountsModule::account_add( + Origin::signed(ADMIN_ACCOUNT_ID), + REGISTRAR_1_ACCOUNT_ID, + super::REGISTRAR_ROLE + ) + ); + assert_ok!( + DSMapsModule::root_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + construct_testing_box(), + coord(DELTA), + ) + ); + let delta: Coord = coord(DELTA); + let waypoints = construct_custom_waypoints("55.373", "37.373", "55.396", "37.386", 10, 20); + let mut testing_rect: Rect2D = construct_testing_rect(); + for _n in 1..11 { + assert_ok!( + DSMapsModule::zone_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + testing_rect, + DEFAULT_HEIGHT, + ROOT_ID, + ) + ); + testing_rect.north_east.lon += delta; + testing_rect.south_west.lon += delta; + } + // Can't add it, one of this zones is blocking the way + assert_noop!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints.clone(), + ROOT_ID, + ), + Error::RouteIntersectRedZone + ); + let zone_index = DSMapsModule::pack_index(ROOT_ID, AREA_ID, 0); + // Now we remove it, clearing the path + assert_ok!( + DSMapsModule::zone_remove( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + zone_index, + ) + ); + assert_ok!( + DSMapsModule::route_add( + Origin::signed(REGISTRAR_1_ACCOUNT_ID), + waypoints, + ROOT_ID, + ) + ); + }); +} \ No newline at end of file diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ef36438..9b43d2c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -50,7 +50,7 @@ pub use pallet_ds_accounts; /// Import the DS maps pallet. pub use pallet_ds_maps; /// Import fixed point for GPS coords -use substrate_fixed::types::I10F22; +use substrate_fixed::types::{I10F22, I42F22}; /// An index to a block. pub type BlockNumber = u32; @@ -314,6 +314,7 @@ impl pallet_ds_maps::Trait for Runtime { type WeightInfo = (); type RawCoord = i32; type Coord = I10F22; + type BigCoord = I42F22; type MaxBuildingsInArea = MaxBuildingsInArea; type MaxHeight = MaxHeight; }