veloren_world/civ/
airship_travel.rs

1use crate::{
2    Index,
3    sim::WorldSim,
4    site::{self, Site, plot::PlotKindMeta},
5    util::{DHashMap, DHashSet, seed_expan},
6};
7use common::{
8    store::{Id, Store},
9    terrain::{MapSizeLg, TERRAIN_CHUNK_BLOCKS_LG},
10    util::Dir,
11};
12use delaunator::{Point, Triangulation, triangulate};
13use itertools::Itertools;
14use rand::{SeedableRng, prelude::*};
15use rand_chacha::ChaChaRng;
16use tracing::{error, warn};
17use vek::*;
18
19#[cfg(debug_assertions)] use tracing::debug;
20
21#[cfg(feature = "airship_maps")]
22use crate::civ::airship_route_map::*;
23
24#[cfg(debug_assertions)]
25macro_rules! debug_airships {
26    ($($arg:tt)*) => {
27        debug!($($arg)*);
28    }
29}
30
31#[cfg(not(debug_assertions))]
32macro_rules! debug_airships {
33    ($($arg:tt)*) => {};
34}
35
36/// A docking position (id, position). The docking position id is
37/// an index of all docking positions in the world.
38#[derive(Clone, Copy, Debug, Default, PartialEq)]
39pub struct AirshipDockingPosition(pub u32, pub Vec3<f32>);
40
41/// The AirshipDock Sites are always oriented along a cardinal direction.
42/// The docking platforms are likewise on the sides of the dock perpendicular
43/// to a cardinal axis.
44#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash)]
45pub enum AirshipDockPlatform {
46    #[default]
47    NorthPlatform,
48    EastPlatform,
49    SouthPlatform,
50    WestPlatform,
51}
52
53/// An airship can dock with its port or starboard side facing the dock.
54#[derive(Debug, Copy, Clone, PartialEq, Default)]
55pub enum AirshipDockingSide {
56    #[default]
57    Port,
58    Starboard,
59}
60
61impl AirshipDockingSide {
62    const EAST_REF_VEC: Vec2<f32> = Vec2 { x: 1.0, y: 0.0 };
63    const NORTH_REF_VEC: Vec2<f32> = Vec2 { x: 0.0, y: 1.0 };
64    const SOUTH_REF_VEC: Vec2<f32> = Vec2 { x: 0.0, y: -1.0 };
65    const WEST_REF_VEC: Vec2<f32> = Vec2 { x: -1.0, y: 0.0 };
66
67    /// When docking, the side to use depends on the angle the airship is
68    /// approaching the dock from, and the platform of the airship dock that
69    /// the airship is docking at.
70    /// For example, when docking at the North Platform:
71    ///
72    /// | From the          |  Docking Side |
73    /// |:----------------- |:--------------|
74    /// | West              |  Starboard    |
75    /// | Northwest         |  Starboard    |
76    /// | North             |  Starboard    |
77    /// | Northeast         |  Port         |
78    /// | East              |  Port         |
79    /// | Southeast         |  Port         |
80    /// | South             |  Port         |
81    /// | Southwest         |  Starboard    |
82    pub fn from_dir_to_platform(dir: &Vec2<f32>, platform: &AirshipDockPlatform) -> Self {
83        // get the reference vector and precompute whether to flip the angle based on
84        // the dir input.
85        let (ref_vec, negate_angle) = match platform {
86            AirshipDockPlatform::NorthPlatform => (&AirshipDockingSide::NORTH_REF_VEC, dir.x < 0.0),
87            AirshipDockPlatform::EastPlatform => (&AirshipDockingSide::EAST_REF_VEC, dir.y > 0.0),
88            AirshipDockPlatform::SouthPlatform => (&AirshipDockingSide::SOUTH_REF_VEC, dir.x > 0.0),
89            AirshipDockPlatform::WestPlatform => (&AirshipDockingSide::WEST_REF_VEC, dir.y < 0.0),
90        };
91        let mut angle = dir.angle_between(*ref_vec).to_degrees();
92        if negate_angle {
93            angle = -angle;
94        }
95        match angle as i32 {
96            -360..=0 => AirshipDockingSide::Port,
97            _ => AirshipDockingSide::Starboard,
98        }
99    }
100}
101
102/// Information needed for an airship to travel to and dock at an AirshipDock
103/// plot.
104#[derive(Clone, Copy, Debug, PartialEq)]
105pub struct AirshipDockingApproach {
106    /// The position of the airship when docked.
107    /// This is different from dock_pos because the airship is offset to align
108    /// the ramp with the dock.
109    pub airship_pos: Vec3<f32>,
110    /// The direction the airship is facing when docked.
111    pub airship_direction: Dir,
112    /// Then center of the AirshipDock Plot.
113    pub dock_center: Vec2<f32>,
114    /// The height above terrain the airship cruises at.
115    pub height: f32,
116    /// The end point of the cruise phase of flight.
117    pub approach_transition_pos: Vec3<f32>,
118    /// There are ramps on both the port and starboard sides of the airship.
119    /// This gives the side that the airship will dock on.
120    pub side: AirshipDockingSide,
121    /// The site name where the airship will be docked at the end of the
122    /// approach.
123    pub site_id: Id<Site>,
124}
125
126/// The docking postions at an AirshipDock plot.
127#[derive(Clone, Debug)]
128pub struct AirshipDockPositions {
129    /// The center of the AirshipDock plot. From the world generation code.
130    pub center: Vec2<f32>,
131    /// The docking positions for the airship, derived from the
132    /// positions calculated in the world generation code.
133    pub docking_positions: Vec<AirshipDockingPosition>,
134    /// The id of the Site where the AirshipDock is located.
135    pub site_id: Id<site::Site>,
136}
137
138/// One leg of an airship route.
139#[derive(Clone, Default, Debug)]
140pub struct AirshipRouteLeg {
141    /// The index of the destination in Airships::docking_positions.
142    pub dest_index: usize,
143    /// The assigned docking platform at the destination dock for this leg.
144    pub platform: AirshipDockPlatform,
145}
146
147/// Information needed for placing an airship in the world when the world is
148/// generated (each time the server starts).
149#[derive(Debug, Clone)]
150pub struct AirshipSpawningLocation {
151    pub pos: Vec2<f32>,
152    pub dir: Vec2<f32>,
153    pub height: f32,
154    pub route_index: usize,
155    pub leg_index: usize,
156}
157
158/// Data for airship operations. This is generated world data.
159#[derive(Clone, Default)]
160pub struct Airships {
161    pub airship_docks: Vec<AirshipDockPositions>,
162    pub routes: Vec<Vec<AirshipRouteLeg>>,
163    pub spawning_locations: Vec<AirshipSpawningLocation>,
164}
165
166// Internal data structures
167
168impl AirshipDockPositions {
169    fn from_plot_meta(
170        first_id: u32,
171        center: Vec2<i32>,
172        docking_positions: &[Vec3<i32>],
173        site_id: Id<site::Site>,
174    ) -> Self {
175        let mut dock_pos_id = first_id;
176        Self {
177            center: center.map(|i| i as f32),
178            docking_positions: docking_positions
179                .iter()
180                .map(|pos: &Vec3<i32>| {
181                    let docking_position =
182                        AirshipDockingPosition(dock_pos_id, pos.map(|i| i as f32));
183                    dock_pos_id += 1;
184                    docking_position
185                })
186                .collect(),
187            site_id,
188        }
189    }
190
191    /// Get the docking position that matches the given platform.
192    fn docking_position(&self, platform: AirshipDockPlatform) -> Vec3<f32> {
193        self.docking_positions
194            .iter()
195            .find_map(|&docking_position| {
196                // The docking position is the one that matches the platform.
197                // The platform is determined by the direction of the docking position
198                // relative to the center of the dock.
199                let docking_position_platform =
200                    AirshipDockPlatform::from_dir(docking_position.1.xy() - self.center);
201                if docking_position_platform == platform {
202                    Some(docking_position.1)
203                } else {
204                    None
205                }
206            })
207            .unwrap_or_else(|| {
208                // If no docking position is found, return the dock center.
209                self.center.with_z(1000.0)
210            })
211    }
212}
213
214// These are used in AirshipDockPlatform::choices_from_dir
215
216static SWEN_PLATFORMS: [AirshipDockPlatform; 4] = [
217    AirshipDockPlatform::SouthPlatform,
218    AirshipDockPlatform::WestPlatform,
219    AirshipDockPlatform::EastPlatform,
220    AirshipDockPlatform::NorthPlatform,
221];
222
223static SEWN_PLATFORMS: [AirshipDockPlatform; 4] = [
224    AirshipDockPlatform::SouthPlatform,
225    AirshipDockPlatform::EastPlatform,
226    AirshipDockPlatform::WestPlatform,
227    AirshipDockPlatform::NorthPlatform,
228];
229
230static WNSE_PLATFORMS: [AirshipDockPlatform; 4] = [
231    AirshipDockPlatform::WestPlatform,
232    AirshipDockPlatform::NorthPlatform,
233    AirshipDockPlatform::SouthPlatform,
234    AirshipDockPlatform::EastPlatform,
235];
236
237static WSNE_PLATFORMS: [AirshipDockPlatform; 4] = [
238    AirshipDockPlatform::WestPlatform,
239    AirshipDockPlatform::SouthPlatform,
240    AirshipDockPlatform::NorthPlatform,
241    AirshipDockPlatform::EastPlatform,
242];
243
244static NEWS_PLATFORMS: [AirshipDockPlatform; 4] = [
245    AirshipDockPlatform::NorthPlatform,
246    AirshipDockPlatform::EastPlatform,
247    AirshipDockPlatform::WestPlatform,
248    AirshipDockPlatform::SouthPlatform,
249];
250
251static NWES_PLATFORMS: [AirshipDockPlatform; 4] = [
252    AirshipDockPlatform::NorthPlatform,
253    AirshipDockPlatform::WestPlatform,
254    AirshipDockPlatform::EastPlatform,
255    AirshipDockPlatform::SouthPlatform,
256];
257
258static ESNW_PLATFORMS: [AirshipDockPlatform; 4] = [
259    AirshipDockPlatform::EastPlatform,
260    AirshipDockPlatform::SouthPlatform,
261    AirshipDockPlatform::NorthPlatform,
262    AirshipDockPlatform::WestPlatform,
263];
264
265static ENSW_PLATFORMS: [AirshipDockPlatform; 4] = [
266    AirshipDockPlatform::EastPlatform,
267    AirshipDockPlatform::NorthPlatform,
268    AirshipDockPlatform::SouthPlatform,
269    AirshipDockPlatform::WestPlatform,
270];
271
272/// The docking platforms used on each leg of the airship route segments is
273/// determined when the routes are generated. Route segments are continuous
274/// loops that are deconflicted by using only one docking platform for any given
275/// leg of a route segment. Since there are four docking platforms per airship
276/// dock, there are at most four route segments passing through a given airship
277/// dock. The docking platforms are also optimized so that on the incoming leg
278/// of a route segment, the airship uses the docking platform that is closest to
279/// the arrival direction (if possible), while still using only one docking
280/// platform per route segment leg.
281impl AirshipDockPlatform {
282    /// Get the preferred docking platform based on the direction vector.
283    pub fn from_dir(dir: Vec2<f32>) -> Self {
284        if let Some(dir) = dir.try_normalized() {
285            let mut angle = dir.angle_between(Vec2::unit_y()).to_degrees();
286            if dir.x < 0.0 {
287                angle = -angle;
288            }
289            match angle as i32 {
290                -360..=-135 => AirshipDockPlatform::SouthPlatform,
291                -134..=-45 => AirshipDockPlatform::WestPlatform,
292                -44..=45 => AirshipDockPlatform::NorthPlatform,
293                46..=135 => AirshipDockPlatform::EastPlatform,
294                136..=360 => AirshipDockPlatform::SouthPlatform,
295                _ => AirshipDockPlatform::NorthPlatform, // should never happen
296            }
297        } else {
298            AirshipDockPlatform::NorthPlatform // default value, should never happen
299        }
300    }
301
302    /// Get the platform choices in order of preference based on the direction
303    /// vector. The first choice is always the most direct plaform given the
304    /// approach direction. Then, the next two choices are the platforms for
305    /// the cardinal directions on each side of the approach direction, and
306    /// the last choice is the platform on the opposite side of the dock.
307    /// The return value is one of the ABCD_PLATFORMS arrays defined above.
308    pub fn choices_from_dir(dir: Vec2<f32>) -> &'static [AirshipDockPlatform] {
309        if let Some(dir) = dir.try_normalized() {
310            let mut angle = dir.angle_between(Vec2::unit_y()).to_degrees();
311            if dir.x < 0.0 {
312                angle = -angle;
313            }
314            // This code works similar to the Direction enum in the common crate.
315            // Angle between produces the smallest angle between two vectors,
316            // so when dir.x is negative, we force the angle to be negative.
317            // 0 or 360 is North. It is assumed that the angle ranges from -360 to 360
318            // degrees even though angles less than -180 or greater than 180
319            // should never be seen.
320            match angle as i32 {
321                -360..=-135 => {
322                    // primary is SouthPlatform
323                    // As a fallback (for when the south platform is already claimed),
324                    // if the direction is more towards the west, use the west platform,
325                    // and if the direction is more towards the east, use the east platform.
326                    // The north platform is always the last resort. All fallback blocks
327                    // below work similarly.
328                    if angle as i32 > -180 {
329                        &SWEN_PLATFORMS
330                    } else {
331                        &SEWN_PLATFORMS
332                    }
333                },
334                -134..=-45 => {
335                    // primary is WestPlatform
336                    if angle as i32 > -90 {
337                        &WNSE_PLATFORMS
338                    } else {
339                        &WSNE_PLATFORMS
340                    }
341                },
342                -44..=45 => {
343                    // primary is NorthPlatform
344                    if angle as i32 > 0 {
345                        &NEWS_PLATFORMS
346                    } else {
347                        &NWES_PLATFORMS
348                    }
349                },
350                46..=135 => {
351                    // primary is EastPlatform
352                    if angle as i32 > 90 {
353                        &ESNW_PLATFORMS
354                    } else {
355                        &ENSW_PLATFORMS
356                    }
357                },
358                136..=360 => {
359                    // primary is SouthPlatform
360                    if angle as i32 > 180 {
361                        &SWEN_PLATFORMS
362                    } else {
363                        &SEWN_PLATFORMS
364                    }
365                },
366                _ => &SEWN_PLATFORMS,
367            }
368        } else {
369            &SEWN_PLATFORMS
370        }
371    }
372
373    /// Get the direction vector that the airship would be facing when docked.
374    fn airship_dir_for_side(&self, side: AirshipDockingSide) -> Dir {
375        match self {
376            AirshipDockPlatform::NorthPlatform => match side {
377                AirshipDockingSide::Starboard => Dir::new(Vec2::unit_x().with_z(0.0)),
378                AirshipDockingSide::Port => Dir::new(-Vec2::unit_x().with_z(0.0)),
379            },
380            AirshipDockPlatform::EastPlatform => match side {
381                AirshipDockingSide::Starboard => Dir::new(-Vec2::unit_y().with_z(0.0)),
382                AirshipDockingSide::Port => Dir::new(Vec2::unit_y().with_z(0.0)),
383            },
384            AirshipDockPlatform::SouthPlatform => match side {
385                AirshipDockingSide::Starboard => Dir::new(-Vec2::unit_x().with_z(0.0)),
386                AirshipDockingSide::Port => Dir::new(Vec2::unit_x().with_z(0.0)),
387            },
388            AirshipDockPlatform::WestPlatform => match side {
389                AirshipDockingSide::Starboard => Dir::new(Vec2::unit_y().with_z(0.0)),
390                AirshipDockingSide::Port => Dir::new(-Vec2::unit_y().with_z(0.0)),
391            },
392        }
393    }
394}
395
396/// A node on the triangulation of the world docking sites, with
397/// data on the nodes that are connected to it.
398#[derive(Clone, Debug, Default, PartialEq)]
399pub struct DockNode {
400    /// An index into the array of all nodes in the graph.
401    pub node_id: usize,
402    /// True if the node is on the outer hull (convex hull) of the
403    /// triangulation.
404    pub on_hull: bool,
405    /// The nodes that are connected to this node.
406    pub connected: Vec<usize>,
407}
408
409impl Airships {
410    /// The nominal distance between airships when they are first spawned in the
411    /// world.
412    pub const AIRSHIP_SPACING: f32 = 5000.0;
413    /// The Z offset between the docking alignment point and the AirshipDock
414    /// plot docking position.
415    const AIRSHIP_TO_DOCK_Z_OFFSET: f32 = -3.0;
416    /// The cruising height varies by route index and there can be only four
417    /// routes.
418    pub const CRUISE_HEIGHTS: [f32; 4] = [400.0, 475.0, 550.0, 625.0];
419    // the generated docking positions in world gen are a little low
420    const DEFAULT_DOCK_DURATION: f32 = 60.0;
421    const DOCKING_TRANSITION_OFFSET: f32 = 175.0;
422    /// The vector from the dock alignment point when the airship is docked on
423    /// the port side.
424    const DOCK_ALIGN_POS_PORT: Vec2<f32> =
425        Vec2::new(Airships::DOCK_ALIGN_X, -Airships::DOCK_ALIGN_Y);
426    /// The vector from the dock alignment point on the airship when the airship
427    /// is docked on the starboard side.
428    const DOCK_ALIGN_POS_STARBOARD: Vec2<f32> =
429        Vec2::new(-Airships::DOCK_ALIGN_X, -Airships::DOCK_ALIGN_Y);
430    // TODO: These alignment offsets are specific to the airship model. If new
431    // models are added, a more generic way to determine the alignment offsets
432    // should be used.
433    /// The absolute offset from the airship's position to the docking alignment
434    /// point on the X axis. The airship is assumed to be facing positive Y.
435    const DOCK_ALIGN_X: f32 = 18.0;
436    /// The offset from the airship's position to the docking alignment point on
437    /// the Y axis. The airship is assumed to be facing positive Y.
438    /// This is positive if the docking alignment point is in front of the
439    /// airship's center position.
440    const DOCK_ALIGN_Y: f32 = 1.0;
441    /// The minimum distance from the docking position where the airship can be
442    /// initially placed in the world.
443    const MIN_SPAWN_POINT_DIST_FROM_DOCK: f32 = 300.0;
444    /// The algorithm that computes where to initially place airships in the
445    /// world (spawning locations) increments the candidate location of the
446    /// first airship on each route by this amount. This is just a prime number
447    /// that is small enough that the AIRSHIP_SPACING is not exceeded by the
448    /// expected number of iterations required to find a starting spawning
449    /// point such that all airships are not too close to docking positions
450    /// when spawned.
451    /// TODO: check that this still if a larger world map is used.
452    const SPAWN_TARGET_DIST_INCREMENT: f32 = 47.0;
453
454    #[inline(always)]
455    pub fn docking_duration() -> f32 { Airships::DEFAULT_DOCK_DURATION }
456
457    /// Get all the airship docking positions from the world sites.
458    fn all_airshipdock_positions(sites: &Store<Site>) -> Vec<AirshipDockPositions> {
459        let mut dock_pos_id = 0;
460        sites
461            .iter()
462            .flat_map(|(site_id, site)| {
463                site.plots().flat_map(move |plot| {
464                    if let Some(PlotKindMeta::AirshipDock {
465                        center,
466                        docking_positions,
467                        ..
468                    }) = plot.kind().meta()
469                    {
470                        Some((center, docking_positions, site_id))
471                    } else {
472                        None
473                    }
474                })
475            })
476            .map(|(center, docking_positions, site_id)| {
477                let positions = AirshipDockPositions::from_plot_meta(
478                    dock_pos_id,
479                    center,
480                    docking_positions,
481                    site_id,
482                );
483
484                dock_pos_id += positions.docking_positions.len() as u32;
485                positions
486            })
487            .collect::<Vec<_>>()
488    }
489
490    /// Convienence function that returns the next route leg accounting for wrap
491    /// around.
492    pub fn increment_route_leg(&self, route_index: usize, leg_index: usize) -> usize {
493        if route_index >= self.routes.len() {
494            error!("Invalid route index: {}", route_index);
495            return 0;
496        }
497        (leg_index + 1) % self.routes[route_index].len()
498    }
499
500    /// Convienence function that returns the previous route leg accounting for
501    /// wrap around.
502    pub fn decrement_route_leg(&self, route_index: usize, leg_index: usize) -> usize {
503        if route_index >= self.routes.len() {
504            error!("Invalid route index: {}", route_index);
505            return 0;
506        }
507        if leg_index > 0 {
508            leg_index - 1
509        } else {
510            self.routes[route_index].len() - 1
511        }
512    }
513
514    /// Convienence function returning the number of routes.
515    pub fn route_count(&self) -> usize { self.routes.len() }
516
517    /// Safe function to get the number of AirshipDock sites on a route.
518    pub fn docking_site_count_for_route(&self, route_index: usize) -> usize {
519        if route_index >= self.routes.len() {
520            error!("Invalid route index: {}", route_index);
521            return 0;
522        }
523        self.routes[route_index].len()
524    }
525
526    /// Assign the docking platforms for each leg of each route. Each route
527    /// consists of a series (Vec) of docking node indices on the docking
528    /// site graph. This function loops over the routes docking nodes and
529    /// assigns a docking platform based on the approach direction to each
530    /// dock node while making sure that no docking platform is used more
531    /// than once (globally, over all routes).
532    fn assign_docking_platforms(
533        route_segments: &[Vec<usize>],
534        dock_locations: &[Vec2<f32>],
535    ) -> Vec<Vec<AirshipRouteLeg>> {
536        let mut incoming_edges = DHashMap::default();
537        for segment in route_segments.iter() {
538            if segment.len() < 3 {
539                continue;
540            }
541            let mut prev_node_id = segment[0];
542            segment.iter().skip(1).for_each(|&node_id| {
543                incoming_edges
544                    .entry(node_id)
545                    .or_insert_with(Vec::new)
546                    .push(prev_node_id);
547                prev_node_id = node_id;
548            });
549        }
550
551        let mut leg_platforms = DHashMap::default();
552
553        incoming_edges.iter().for_each(|(node_id, edges)| {
554            let dock_location = dock_locations[*node_id];
555            let mut used_platforms = DHashSet::default();
556            for origin in edges {
557                let origin_location = dock_locations[*origin];
558                // Determine the platform to dock using the direction from the dock location
559                // to the origin location
560                let rev_approach_dir = origin_location - dock_location;
561                let docking_platforms = AirshipDockPlatform::choices_from_dir(rev_approach_dir);
562                let docking_platform = docking_platforms
563                    .iter()
564                    .find(|&platform| !used_platforms.contains(platform))
565                    .copied()
566                    .unwrap_or(AirshipDockPlatform::NorthPlatform);
567                leg_platforms.insert((*origin, *node_id), docking_platform);
568                used_platforms.insert(docking_platform);
569            }
570        });
571
572        #[cfg(debug_assertions)]
573        {
574            debug!("Route segments: {:?}", route_segments);
575            debug!("Leg platforms: {:?}", leg_platforms);
576        }
577
578        // The incoming edges control the docking platforms used for each leg of the
579        // route. The outgoing platform for leg i must match the incoming
580        // platform for leg i-1. For the first leg, get the 'from' platform from
581        // the last pair of nodes in the segment.
582        let mut routes = Vec::new();
583        route_segments.iter().for_each(|segment| {
584            assert!(
585                segment.len() > 2,
586                "Segments must have at least two nodes and they must wrap around."
587            );
588            let mut route_legs = Vec::new();
589            let leg_start = &segment[segment.len() - 2..];
590            for leg_index in 0..segment.len() - 1 {
591                let from_node = segment[leg_index];
592                let to_node = segment[leg_index + 1];
593                if leg_index == 0 {
594                    assert!(
595                        from_node == leg_start[1],
596                        "The 'previous' leg's 'to' node must match the current leg's 'from' node."
597                    );
598                }
599                let to_platform = leg_platforms.get(&(from_node, to_node)).copied().unwrap_or(
600                    AirshipDockPlatform::from_dir(
601                        dock_locations[from_node] - dock_locations[to_node],
602                    ),
603                );
604                route_legs.push(AirshipRouteLeg {
605                    dest_index: to_node,
606                    platform: to_platform,
607                });
608            }
609            routes.push(route_legs);
610        });
611        routes
612    }
613
614    /// For each route, calculate the location along the route where airships
615    /// should be initially located when the server starts. This attempts to
616    /// space airships evenly along the route while ensuring that no airship
617    /// is too close to a docking position. Each airship needs separation
618    /// from the docking position such that the airship can initially move
619    /// forward towards its target docking location when it first starts
620    /// flying because it will start in the cruise phase of flight.
621    pub fn calculate_spawning_locations(&mut self, all_dock_points: &[Point]) {
622        let mut spawning_locations = Vec::new();
623        let mut expected_airships_count = 0;
624        self.routes
625            .iter()
626            .enumerate()
627            .for_each(|(route_index, route)| {
628                // Get the route length in blocks.
629                let route_len_blocks = route.iter().enumerate().fold(0.0f64, |acc, (j, leg)| {
630                    let to_dock_point = &all_dock_points[leg.dest_index];
631                    let from_dock_point = if j > 0 {
632                        &all_dock_points[route[j - 1].dest_index]
633                    } else {
634                        &all_dock_points[route[route.len() - 1].dest_index]
635                    };
636                    let from_loc = Vec2::new(from_dock_point.x as f32, from_dock_point.y as f32);
637                    let to_loc = Vec2::new(to_dock_point.x as f32, to_dock_point.y as f32);
638                    acc + from_loc.distance(to_loc) as f64
639                });
640                // The minimum number of airships to spawn on this route is the number of
641                // docking sites. The maximum is where airships would be spaced
642                // out evenly with Airships::AIRSHIP_SPACING blocks between them.
643                let airship_count = route
644                    .len()
645                    .max((route_len_blocks / Airships::AIRSHIP_SPACING as f64) as usize);
646
647                // Keep track of the total number of airships expected to be spawned.
648                expected_airships_count += airship_count;
649
650                // The precise desired airship spacing.
651                let airship_spacing = (route_len_blocks / airship_count as f64) as f32;
652                debug_airships!(
653                    "Route {} length: {} blocks, avg: {}, expecting {} airships for {} docking \
654                     sites",
655                    route_index,
656                    route_len_blocks,
657                    route_len_blocks / route.len() as f64,
658                    airship_count,
659                    route.len()
660                );
661
662                // get the docking points on this route
663                let route_points = route
664                    .iter()
665                    .map(|leg| all_dock_points[leg.dest_index].clone())
666                    .collect::<Vec<_>>();
667
668                // Airships can't be spawned too close to the docking sites. The leg lengths and
669                // desired spacing between airships will probably cause spawning
670                // locations to violate the too close rule, so
671                // do some iterations where the initial spawning location is varied, and the
672                // spawning locations are corrected to be at least
673                // Airships::MIN_SPAWN_POINT_DIST_FROM_DOCK blocks from docking sites.
674                // Keep track of the deviation from the ideal positions and then use the
675                // spawning locations that produce the least deviation.
676                let mut best_route_spawning_locations = Vec::new();
677                let mut best_route_deviations = f32::MAX;
678                #[cfg(debug_assertions)]
679                let mut best_route_iteration = 0;
680                // 50 iterations works for the test data, but it may need to be adjusted
681                for i in 0..50 {
682                    let mut route_spawning_locations = Vec::new();
683                    let mut prev_point = &route_points[route_points.len() - 1];
684                    let mut target_dist = -1.0;
685                    let mut airships_spawned = 0;
686                    let mut deviation = 0.0;
687                    route_points
688                        .iter()
689                        .enumerate()
690                        .for_each(|(leg_index, dock_point)| {
691                            let to_loc = Vec2::new(dock_point.x as f32, dock_point.y as f32);
692                            let from_loc = Vec2::new(prev_point.x as f32, prev_point.y as f32);
693                            let leg_dir = (to_loc - from_loc).normalized();
694                            let leg_len = from_loc.distance(to_loc);
695                            // target_dist is the distance from the 'from' docking position where
696                            // the airship should spawn. The maximum is
697                            // the length of the leg minus the minimum spawn distance from the dock.
698                            // The minimum is the minimum spawn distance from the dock.
699                            let max_target_dist =
700                                leg_len - Airships::MIN_SPAWN_POINT_DIST_FROM_DOCK;
701                            // Each iteration, the initial target distance is incremented by a prime
702                            // number. If more than 50 iterations are
703                            // needed, SPAWN_TARGET_DIST_INCREMENT might need to be reduced
704                            // so that the initial target distance doesn't exceed the length of the
705                            // first leg of the route.
706                            if target_dist < 0.0 {
707                                target_dist = Airships::MIN_SPAWN_POINT_DIST_FROM_DOCK
708                                    + i as f32 * Airships::SPAWN_TARGET_DIST_INCREMENT;
709                            }
710                            // When target_dist exceeds the leg length, it means the spawning
711                            // location is into the next leg.
712                            while target_dist <= leg_len {
713                                // Limit the actual spawn location and keep track of the deviation.
714                                let spawn_point_dist = if target_dist > max_target_dist {
715                                    deviation += target_dist - max_target_dist;
716                                    max_target_dist
717                                } else if target_dist < Airships::MIN_SPAWN_POINT_DIST_FROM_DOCK {
718                                    deviation +=
719                                        Airships::MIN_SPAWN_POINT_DIST_FROM_DOCK - target_dist;
720                                    Airships::MIN_SPAWN_POINT_DIST_FROM_DOCK
721                                } else {
722                                    target_dist
723                                };
724
725                                let spawn_loc = from_loc + leg_dir * spawn_point_dist;
726                                route_spawning_locations.push(AirshipSpawningLocation {
727                                    pos: Vec2::new(spawn_loc.x, spawn_loc.y),
728                                    dir: leg_dir,
729                                    height: Airships::CRUISE_HEIGHTS
730                                        [route_index % Airships::CRUISE_HEIGHTS.len()],
731                                    route_index,
732                                    leg_index,
733                                });
734                                airships_spawned += 1;
735                                target_dist += airship_spacing;
736                            }
737                            target_dist -= leg_len;
738                            assert!(
739                                target_dist > 0.0,
740                                "Target distance should not be zero or negative: {}",
741                                target_dist
742                            );
743                            prev_point = dock_point;
744                        });
745                    if deviation < best_route_deviations {
746                        best_route_deviations = deviation;
747                        best_route_spawning_locations = route_spawning_locations.clone();
748                        #[cfg(debug_assertions)]
749                        {
750                            best_route_iteration = i;
751                        }
752                    }
753                }
754                debug_airships!(
755                    "Route {}: {} airships, {} spawning locations, best deviation: {}, iteration: \
756                     {}",
757                    route_index,
758                    airship_count,
759                    best_route_spawning_locations.len(),
760                    best_route_deviations,
761                    best_route_iteration
762                );
763                spawning_locations.extend(best_route_spawning_locations);
764            });
765        debug_airships!(
766            "Set spawning locations for {} airships of {} expected",
767            spawning_locations.len(),
768            expected_airships_count
769        );
770        if spawning_locations.len() == expected_airships_count {
771            self.spawning_locations = spawning_locations;
772            debug_airships!("Spawning locations: {:?}", self.spawning_locations);
773        } else {
774            error!(
775                "Expected {} airships, but produced only {} spawning locations.",
776                expected_airships_count,
777                spawning_locations.len()
778            );
779        }
780    }
781
782    /// Generates the airship routes.
783    pub fn generate_airship_routes_inner(
784        &mut self,
785        map_size_lg: &MapSizeLg,
786        seed: u32,
787        _index: Option<&Index>,
788        _sampler: Option<&WorldSim>,
789        _map_image_path: Option<&str>,
790    ) {
791        let all_dock_points = self
792            .airship_docks
793            .iter()
794            .map(|dock| Point {
795                x: dock.center.x as f64,
796                y: dock.center.y as f64,
797            })
798            .collect::<Vec<_>>();
799        debug_airships!("all_dock_points: {:?}", all_dock_points);
800
801        // Run the delaunay triangulation on the docking points.
802        let triangulation = triangulate(&all_dock_points);
803
804        #[cfg(feature = "airship_maps")]
805        save_airship_routes_triangulation(
806            &triangulation,
807            &all_dock_points,
808            map_size_lg,
809            seed,
810            _index,
811            _sampler,
812            _map_image_path,
813        );
814
815        // Docking positions are specified in world coordinates, not chunks.
816        // Limit the max route leg length to 1000 chunks no matter the world size.
817        let blocks_per_chunk = 1 << TERRAIN_CHUNK_BLOCKS_LG;
818        let world_blocks = map_size_lg.chunks().map(|u| u as f32) * blocks_per_chunk as f32;
819        let max_route_leg_length = 1000.0 * world_blocks.x;
820
821        // eulerized_route_segments is fairly expensive as the number of docking sites
822        // grows. Limit the number of iterations according to world size.
823        // pow2     world size   iterations
824        // 10       1024         50
825        // 11       2048         22
826        // 12       4096         10
827        // 13       8192          2
828        // Doing a least squares fit on the iterations gives the formula:
829        // 3742931.0 * e.powf(-1.113823 * pow2)
830        // 3742931.0 * 2.71828f32.powf(-1.113823 * pow2)
831
832        let pow2 = map_size_lg.vec().x;
833        let max_iterations = (3742931.0 * std::f32::consts::E.powf(-1.113823 * pow2 as f32))
834            .clamp(1.0, 100.0)
835            .round() as usize;
836
837        if let Some((best_segments, _, _max_seg_len, _min_spread, _iteration)) = triangulation
838            .eulerized_route_segments(
839                &all_dock_points,
840                max_iterations,
841                max_route_leg_length as f64,
842                seed,
843            )
844        {
845            #[cfg(debug_assertions)]
846            {
847                debug!("Max segment length: {}", _max_seg_len);
848                debug!("Min spread: {}", _min_spread);
849                debug!("Iteration: {}", _iteration);
850                debug!("Segments count:");
851                best_segments.iter().enumerate().for_each(|segment| {
852                    debug!("  {} : {}", segment.0, segment.1.len());
853                });
854                debug!("Best segments: {:?}", best_segments);
855                #[cfg(feature = "airship_maps")]
856                if let Some(index) = _index
857                    && let Some(world_sim) = _sampler
858                    && let Err(e) = export_world_map(index, world_sim)
859                {
860                    eprintln!("Failed to export world map: {:?}", e);
861                }
862            }
863
864            self.routes = Airships::assign_docking_platforms(
865                &best_segments,
866                all_dock_points
867                    .iter()
868                    .map(|p| Vec2::new(p.x as f32, p.y as f32))
869                    .collect::<Vec<_>>()
870                    .as_slice(),
871            );
872
873            // Calculate the spawning locations for airships on the routes.
874            self.calculate_spawning_locations(&all_dock_points);
875
876            #[cfg(feature = "airship_maps")]
877            save_airship_route_segments(
878                &self.routes,
879                &all_dock_points,
880                &self.spawning_locations,
881                map_size_lg,
882                seed,
883                _index,
884                _sampler,
885                _map_image_path,
886            );
887        } else {
888            eprintln!("Error - cannot eulerize the dock points.");
889        }
890    }
891
892    pub fn generate_airship_routes(&mut self, world_sim: &mut WorldSim, index: &Index) {
893        self.airship_docks = Airships::all_airshipdock_positions(&index.sites);
894
895        self.generate_airship_routes_inner(
896            &world_sim.map_size_lg(),
897            index.seed,
898            Some(index),
899            Some(world_sim),
900            None,
901        );
902    }
903
904    /// Compute the transition point where the airship should stop the cruise
905    /// flight phase and start the docking phase.
906    /// ```text
907    ///  F : From position
908    ///  T : Transition point
909    ///  D : Docking position
910    ///  C : Center of the airship dock
911    ///  X : Airship dock
912    ///
913    ///                      F
914    ///                     ∙
915    ///                    ∙
916    ///                   ∙
917    ///                  ∙
918    ///                 ∙
919    ///                T
920    ///               ∙
921    ///              ∙
922    ///             D
923    ///
924    ///           XXXXX
925    ///         XX     XX
926    ///        X         X
927    ///        X    C    X
928    ///        X         X
929    ///         XX     XX
930    ///           XXXXX
931    /// ```
932    /// The transition point between cruise flight and docking is on a line
933    /// between the route leg starting point (F) and the docking position
934    /// (D), short of the docking position by
935    /// Airships::DOCKING_TRANSITION_OFFSET blocks.
936    ///
937    /// # Arguments
938    ///
939    /// * `dock_index` - The airship dock index in airship_docks.
940    /// * `route_index` - The index of the route (outer vector of
941    ///   airships.routes). This is used to determine the cruise height.
942    /// * `platform` - The platform on the airship dock where the airship is to
943    ///   dock.
944    /// * `from` - The position from which the airship is approaching the dock.
945    ///   I.e., the position of the dock for the previous route leg.
946    /// # Returns
947    /// The 2D position calculated with the Z coordinate set to the
948    /// docking_position.z + cruise height.
949    pub fn approach_transition_point(
950        &self,
951        dock_index: usize,
952        route_index: usize,
953        platform: AirshipDockPlatform,
954        from: Vec2<f32>,
955    ) -> Option<Vec3<f32>> {
956        if let Some(dock_pos) = self.airship_docks.get(dock_index) {
957            let docking_position = dock_pos.docking_position(platform);
958            let dir = (docking_position.xy() - from).normalized();
959            return Some(
960                (docking_position.xy() - dir * Airships::DOCKING_TRANSITION_OFFSET)
961                    .with_z(docking_position.z + Airships::CRUISE_HEIGHTS[route_index]),
962            );
963        }
964        warn!(
965            "Approach point invalid, no airship dock found for docking position index {}",
966            dock_index
967        );
968        None
969    }
970
971    fn vec3_relative_eq(a: &vek::Vec3<f32>, b: &vek::Vec3<f32>, epsilon: f32) -> bool {
972        (a.x - b.x).abs() < epsilon && (a.y - b.y).abs() < epsilon && (a.z - b.z).abs() < epsilon
973    }
974
975    pub fn approach_for_route_and_leg(
976        &self,
977        route_index: usize,
978        leg_index: usize,
979    ) -> AirshipDockingApproach {
980        // Get the docking positions for the route and leg.
981        let to_route_leg = &self.routes[route_index][leg_index];
982        let from_route_leg = if leg_index == 0 {
983            &self.routes[route_index][self.routes[route_index].len() - 1]
984        } else {
985            &self.routes[route_index][leg_index - 1]
986        };
987        let dest_dock_positions = &self.airship_docks[to_route_leg.dest_index];
988        let from_dock_positions = &self.airship_docks[from_route_leg.dest_index];
989
990        let docking_side = AirshipDockingSide::from_dir_to_platform(
991            &(dest_dock_positions.center - from_dock_positions.center),
992            &to_route_leg.platform,
993        );
994
995        let (airship_pos, airship_direction) = Airships::airship_vec_for_docking_pos(
996            dest_dock_positions.docking_position(to_route_leg.platform),
997            dest_dock_positions.center,
998            Some(docking_side),
999        );
1000
1001        AirshipDockingApproach {
1002            airship_pos,
1003            airship_direction,
1004            dock_center: dest_dock_positions.center,
1005            height: Airships::CRUISE_HEIGHTS[route_index],
1006            approach_transition_pos: self
1007                .approach_transition_point(
1008                    to_route_leg.dest_index,
1009                    route_index,
1010                    to_route_leg.platform,
1011                    from_dock_positions.center,
1012                )
1013                .unwrap_or_else(|| {
1014                    warn!(
1015                        "Failed to calculate approach transition point for route {} leg {}",
1016                        route_index, leg_index
1017                    );
1018                    dest_dock_positions.docking_position(to_route_leg.platform)
1019                }),
1020            side: docking_side,
1021            site_id: dest_dock_positions.site_id,
1022        }
1023    }
1024
1025    pub fn airship_spawning_locations(&self) -> Vec<AirshipSpawningLocation> {
1026        self.spawning_locations.clone()
1027    }
1028
1029    /// Get the position a route leg originates from.
1030    pub fn route_leg_departure_location(&self, route_index: usize, leg_index: usize) -> Vec2<f32> {
1031        if route_index >= self.routes.len() || leg_index >= self.routes[route_index].len() {
1032            error!("Invalid index: rt {}, leg {}", route_index, leg_index);
1033            return Vec2::zero();
1034        }
1035
1036        let prev_leg = if leg_index == 0 {
1037            &self.routes[route_index][self.routes[route_index].len() - 1]
1038        } else {
1039            &self.routes[route_index][leg_index - 1]
1040        };
1041
1042        self.airship_docks[prev_leg.dest_index]
1043            .docking_position(prev_leg.platform)
1044            .xy()
1045    }
1046
1047    /// Get the position and direction for the airship to dock at the given
1048    /// docking position. If use_starboard_boarding is None, the side for
1049    /// boarding is randomly chosen. The center of the airship position with
1050    /// respect to the docking position is an asymmetrical offset depending on
1051    /// which side of the airship will be used for boarding and where the
1052    /// captain is located on the airship. The returned position is the
1053    /// position where the captain will be when the airship is docked
1054    /// (because the captain NPC is the one that is positioned in the agent
1055    /// or rtsim code).
1056    pub fn airship_vec_for_docking_pos(
1057        docking_pos: Vec3<f32>,
1058        airship_dock_center: Vec2<f32>,
1059        docking_side: Option<AirshipDockingSide>,
1060    ) -> (Vec3<f32>, Dir) {
1061        // choose a random side for docking if not specified
1062        let dock_side = docking_side.unwrap_or_else(|| {
1063            if rand::rng().random::<bool>() {
1064                AirshipDockingSide::Starboard
1065            } else {
1066                AirshipDockingSide::Port
1067            }
1068        });
1069        // Get the vector from the dock alignment position on the airship to the
1070        // captain's position and the rotation angle for the ship to dock on the
1071        // specified side. The dock alignment position is where the airship
1072        // should touch or come closest to the dock. The side_rotation is the
1073        // angle the ship needs to rotate from to be perpendicular to the vector
1074        // from the dock center to the docking position. For example, if the docking
1075        // position is directly north (0 degrees, or aligned with the unit_y
1076        // vector), the ship needs to rotate 90 degrees CCW to dock on the port
1077        // side or 270 degrees CCW to dock on the starboard side.
1078        let (dock_align_to_captain, side_rotation) = if dock_side == AirshipDockingSide::Starboard {
1079            (
1080                Airships::DOCK_ALIGN_POS_STARBOARD,
1081                3.0 * std::f32::consts::FRAC_PI_2,
1082            )
1083        } else {
1084            (Airships::DOCK_ALIGN_POS_PORT, std::f32::consts::FRAC_PI_2)
1085        };
1086        // get the vector from the dock center to the docking platform point where the
1087        // airship should touch or come closest to.
1088        let dock_pos_offset = (docking_pos - airship_dock_center).xy();
1089        // The airship direction when docked is the dock_pos_offset rotated by the
1090        // side_rotation angle.
1091        let airship_dir =
1092            Dir::from_unnormalized(dock_pos_offset.rotated_z(side_rotation).with_z(0.0))
1093                .unwrap_or_default();
1094        // The dock_align_to_captain vector is rotated by the angle between unit_y and
1095        // the airship direction.
1096        let ship_dock_rotation =
1097            Airships::angle_between_vectors_ccw(Vec2::unit_y(), airship_dir.vec().xy());
1098        let captain_offset = dock_align_to_captain
1099            .rotated_z(ship_dock_rotation)
1100            .with_z(Airships::AIRSHIP_TO_DOCK_Z_OFFSET);
1101
1102        // To get the location of the pilot when the ship is docked, add the
1103        // captain_offset to the docking position.
1104        (docking_pos + captain_offset, airship_dir)
1105    }
1106
1107    /// Returns the angle from vec v1 to vec v2 in the CCW direction.
1108    pub fn angle_between_vectors_ccw(v1: Vec2<f32>, v2: Vec2<f32>) -> f32 {
1109        let dot_product = v1.dot(v2);
1110        let det = v1.x * v2.y - v1.y * v2.x; // determinant
1111        let angle = det.atan2(dot_product); // atan2(det, dot_product) gives the CCW angle
1112        if angle < 0.0 {
1113            angle + std::f32::consts::TAU
1114        } else {
1115            angle
1116        }
1117    }
1118
1119    /// Returns the angle from vec v1 to vec v2 in the CW direction.
1120    pub fn angle_between_vectors_cw(v1: Vec2<f32>, v2: Vec2<f32>) -> f32 {
1121        let ccw_angle = Airships::angle_between_vectors_ccw(v1, v2);
1122        std::f32::consts::TAU - ccw_angle
1123    }
1124}
1125
1126#[cfg(debug_assertions)]
1127macro_rules! debug_airship_eulerization {
1128    ($($arg:tt)*) => {
1129        debug!($($arg)*);
1130    }
1131}
1132
1133#[cfg(not(debug_assertions))]
1134macro_rules! debug_airship_eulerization {
1135    ($($arg:tt)*) => {};
1136}
1137
1138/// A map of node index to DockNode, where DockNode contains a list of
1139/// nodes that the node is connected to.
1140type DockNodeGraph = DHashMap<usize, DockNode>;
1141
1142/// Extension functions for Triangulation (from the triangulate crate).
1143trait TriangulationExt {
1144    fn all_edges(&self) -> DHashSet<(usize, usize)>;
1145    fn is_hull_node(&self, index: usize) -> bool;
1146    fn node_connections(&self) -> DockNodeGraph;
1147    fn eulerized_route_segments(
1148        &self,
1149        all_dock_points: &[Point],
1150        iterations: usize,
1151        max_route_leg_length: f64,
1152        seed: u32,
1153    ) -> Option<(Vec<Vec<usize>>, Vec<usize>, usize, f32, usize)>;
1154}
1155
1156/// Find the first node in the graph where the DockNode has an odd number of
1157/// connections to other nodes.
1158fn first_odd_node(
1159    search_order: &[usize],
1160    start: usize,
1161    nodes: &DockNodeGraph,
1162) -> Option<(usize, usize)> {
1163    search_order
1164        .iter()
1165        .enumerate()
1166        .skip(start)
1167        .find_map(|(index, &node_index)| {
1168            if let Some(dock_node) = nodes.get(&node_index) {
1169                if dock_node.connected.len() % 2 == 1 {
1170                    Some((index, node_index))
1171                } else {
1172                    None
1173                }
1174            } else {
1175                None
1176            }
1177        })
1178}
1179
1180/// Removes an edge between two nodes in the tesselation graph.
1181fn remove_edge(edge: (usize, usize), nodes: &mut DockNodeGraph) {
1182    if let Some(dock_node) = nodes.get_mut(&edge.0) {
1183        // Remove the edge from node_id1 to node_id2.
1184        // The edge may be present more than once, just remove one instance.
1185        if let Some(index) = dock_node
1186            .connected
1187            .iter()
1188            .position(|&node_id| node_id == edge.1)
1189        {
1190            dock_node.connected.remove(index);
1191        }
1192    }
1193    if let Some(dock_node) = nodes.get_mut(&edge.1) {
1194        // Remove the edge from node_id2 to node_id1.
1195        // The edge may be present more than once, just remove one instance.
1196        if let Some(index) = dock_node
1197            .connected
1198            .iter()
1199            .position(|&node_id| node_id == edge.0)
1200        {
1201            dock_node.connected.remove(index);
1202        }
1203    }
1204}
1205
1206/// Adds an edge between two nodes in the tesselation graph.
1207fn add_edge(edge: (usize, usize), nodes: &mut DockNodeGraph) {
1208    if let Some(dock_node) = nodes.get_mut(&edge.0) {
1209        dock_node.connected.push(edge.1);
1210    }
1211    if let Some(dock_node) = nodes.get_mut(&edge.1) {
1212        dock_node.connected.push(edge.0);
1213    }
1214}
1215
1216/// Implementation of extension functions for the Triangulation struct.
1217impl TriangulationExt for Triangulation {
1218    /// Get the set of all edges in the triangulation.
1219    fn all_edges(&self) -> DHashSet<(usize, usize)> {
1220        let mut edges = DHashSet::default();
1221        for t in self.triangles.chunks(3) {
1222            let a = t[0];
1223            let b = t[1];
1224            let c = t[2];
1225            // The edges hashset must have edges specified in increasing order to avoid
1226            // duplicates.
1227            edges.insert(if a < b { (a, b) } else { (b, a) });
1228            edges.insert(if b < c { (b, c) } else { (c, b) });
1229            edges.insert(if a < c { (a, c) } else { (c, a) });
1230        }
1231        edges
1232    }
1233
1234    /// For all triangles in the tessellation, create a map of nodes to their
1235    /// connected nodes.
1236    fn node_connections(&self) -> DockNodeGraph {
1237        let mut connections = DHashMap::default();
1238
1239        self.triangles.chunks(3).for_each(|t| {
1240            for &node in t {
1241                let dock_node = connections.entry(node).or_insert_with(|| DockNode {
1242                    node_id: node,
1243                    on_hull: self.is_hull_node(node),
1244                    connected: Vec::default(),
1245                });
1246                for &connected_node in t {
1247                    if connected_node != node && !dock_node.connected.contains(&connected_node) {
1248                        dock_node.connected.push(connected_node);
1249                    }
1250                }
1251            }
1252        });
1253        for (_, dock_node) in connections.iter_mut() {
1254            dock_node.connected = dock_node.connected.to_vec();
1255        }
1256        connections
1257    }
1258
1259    /// True if the node is on the outer hull of the triangulation.
1260    fn is_hull_node(&self, index: usize) -> bool { self.hull.contains(&index) }
1261
1262    /// Calculates the best way to modify the triangulation so that
1263    /// all nodes have an even number of connections (all nodes have
1264    /// an even 'degree'). The steps are:
1265    ///
1266    /// 1. Remove very long edges (not important for eurelization, but this is a
1267    ///    goal of the airship routes design.
1268    /// 2. Remove the shortest edges from all nodes that have more than 8
1269    ///    connections to other nodes. This is because the airship docking sites
1270    ///    have at most 4 docking positions, and for deconfliction purposes, no
1271    ///    two "routes" can use the same docking position.
1272    /// 3. Add edges to the triangulation so that all nodes have an even number
1273    ///    of connections to other nodes. There are many combinations of added
1274    ///    edges that can make all nodes have an even number of connections. The
1275    ///    goal is to find a graph with the maximum number of 'routes'
1276    ///    (sub-graphs of connected nodes that form a closed loop), where the
1277    ///    routes are all the same length. Since this is a graph, the algorithm
1278    ///    is sensitive to the starting point. Several iterations are tried with
1279    ///    different starting points (node indices), and the best result is
1280    ///    returned.
1281    ///
1282    /// Returns a tuple with the following elements:
1283    ///  - best_route_segments (up to 4 routes, each route is a vector of node
1284    ///    indices)
1285    ///  - best_circuit (the full eulerian circuit)
1286    ///  - max_seg_len (the length of the longest route segment)
1287    ///  - min_spread (the standard deviation of the route segment lengths)
1288    ///  - best_iteration (for debugging, the iteration that produced the best
1289    ///    result)
1290    fn eulerized_route_segments(
1291        &self,
1292        all_dock_points: &[Point],
1293        iterations: usize,
1294        max_route_leg_length: f64,
1295        seed: u32,
1296    ) -> Option<(Vec<Vec<usize>>, Vec<usize>, usize, f32, usize)> {
1297        let mut edges_to_remove = DHashSet::default();
1298
1299        // There can be at most four incoming and four outgoing edges per node because
1300        // there are only four docking positions per docking site and for deconfliction
1301        // purposes, no two "routes" can use the same docking position. This means that
1302        // the maximum number of edges per node is 8. Remove the shortest edges from
1303        // nodes with more than 8 edges.
1304
1305        // The tessellation algorithm produces convex hull, and there can be edges
1306        // connecting outside nodes where the distance between the points is a
1307        // significant fraction of the hull diameter. We want to keep airship
1308        // route legs as short as possible, while not removing interior edges
1309        // that may already be fairly long due to the configuration of the
1310        // docking sites relative to the entire map. For the standard world map,
1311        // with 2^10 chunks (1024x1024), the hull diameter is about 1000 chunks.
1312        // Experimentally, the standard world map can have interior edges that are
1313        // around 800 chunks long. A world map with 2^12 chunks (4096x4096) can
1314        // have hull edges that are around 2000 chunks long, but interior edges
1315        // still have a max of around 800 chunks. For the larger world maps,
1316        // removing edges that are longer than 1000 chunks is a good heuristic.
1317
1318        // First, use these heuristics to remove excess edges from the node graph.
1319        // 1. remove edges that are longer than 1000 blocks.
1320        // 2. remove the shortest edges from nodes with more than 8 edges
1321
1322        let max_distance_squared = max_route_leg_length.powi(2);
1323
1324        let all_edges = self.all_edges();
1325        for edge in all_edges.iter() {
1326            let pt1 = &all_dock_points[edge.0];
1327            let pt2 = &all_dock_points[edge.1];
1328            let v1 = Vec2 { x: pt1.x, y: pt1.y };
1329            let v2 = Vec2 { x: pt2.x, y: pt2.y };
1330            // Remove the edge if the distance between the points is greater than
1331            // max_leg_length
1332            if v1.distance_squared(v2) > max_distance_squared {
1333                edges_to_remove.insert(*edge);
1334            }
1335        }
1336
1337        #[cfg(debug_assertions)]
1338        let long_edges = edges_to_remove.len();
1339
1340        debug_airship_eulerization!(
1341            "Found {} long edges to remove out of {} total edges",
1342            edges_to_remove.len(),
1343            all_edges.len()
1344        );
1345
1346        let node_connections = self.node_connections();
1347        node_connections.iter().for_each(|(&node_id, node)| {
1348            if node.connected.len() > 8 {
1349                let excess_edges_count = node.connected.len() - 8;
1350                // Find the shortest edge and remove it
1351                let mut connected_node_info = node
1352                    .connected
1353                    .iter()
1354                    .map(|&connected_node_id| {
1355                        let pt1 = &all_dock_points[node_id];
1356                        let pt2 = &all_dock_points[connected_node_id];
1357                        let v1 = Vec2 { x: pt1.x, y: pt1.y };
1358                        let v2 = Vec2 { x: pt2.x, y: pt2.y };
1359                        (connected_node_id, v1.distance_squared(v2) as i64)
1360                    })
1361                    .collect::<Vec<_>>();
1362                connected_node_info.sort_by(|a, b| a.1.cmp(&b.1));
1363                let mut excess_edges_remaining = excess_edges_count;
1364                let mut remove_index = 0;
1365                while excess_edges_remaining > 0 && remove_index < connected_node_info.len() {
1366                    let (connected_node_id, _) = connected_node_info[remove_index];
1367                    let edge = if node_id < connected_node_id {
1368                        (node_id, connected_node_id)
1369                    } else {
1370                        (connected_node_id, node_id)
1371                    };
1372                    if !edges_to_remove.contains(&edge) {
1373                        edges_to_remove.insert(edge);
1374                        excess_edges_remaining -= 1;
1375                    }
1376                    remove_index += 1;
1377                }
1378            }
1379        });
1380
1381        let mut mutable_node_connections = node_connections.clone();
1382
1383        debug_airship_eulerization!(
1384            "Removing {} long edges and {} excess edges for a total of {} removed edges out of a \
1385             total of {} edges",
1386            long_edges,
1387            edges_to_remove.len() - long_edges,
1388            edges_to_remove.len(),
1389            all_edges.len(),
1390        );
1391
1392        for edge in edges_to_remove {
1393            remove_edge(edge, &mut mutable_node_connections);
1394        }
1395
1396        #[cfg(debug_assertions)]
1397        {
1398            // count the number of nodes with an odd connected count
1399            let odd_connected_count0 = mutable_node_connections
1400                .iter()
1401                .filter(|(_, node)| node.connected.len() % 2 == 1)
1402                .count();
1403            let total_connections1 = mutable_node_connections
1404                .iter()
1405                .map(|(_, node)| node.connected.len())
1406                .sum::<usize>();
1407            debug_airship_eulerization!(
1408                "After Removing, odd connected count: {} in {} nodes, total connections: {}",
1409                odd_connected_count0,
1410                mutable_node_connections.len(),
1411                total_connections1
1412            );
1413        }
1414
1415        // Now eurlerize the node graph by adding edges to connect nodes with an odd
1416        // number of connections. Eurlerization means that every node will have
1417        // an even number of degrees (edges), which is a requirement for
1418        // creating a Eulerian Circuit.
1419
1420        // Get the keys (node ids, which is the same as the node's index in the
1421        // all_dock_points vector) of nodes with an odd number of edges.
1422        let mut odd_keys: Vec<usize> = mutable_node_connections
1423            .iter()
1424            .filter(|(_, node)| node.connected.len() % 2 == 1)
1425            .map(|(node_id, _)| *node_id)
1426            .collect();
1427
1428        let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
1429
1430        // There will always be an even number of odd nodes in a connected graph (one
1431        // where all nodes are reachable from any other node). The goal is to
1432        // pair the odd nodes, adding edges between each pair such that the
1433        // added edges are as short as possible. After adding edges, the graph
1434        // will have an even number of edges for each node.
1435
1436        // The starting node index for finding pairs is arbitrary, and starting from
1437        // different nodes will yield different Eulerian circuits.
1438
1439        // Do a number of iterations and find the best results. The criteria is
1440        // 1. The number of route groups (the outer Vec in best_route_segments) This
1441        //    will be a maximum of 4 because there are at most 4 docking positions per
1442        //    docking site. More is better.
1443        // 2. The 'spread' of the lengths of the inner Vecs in best_route_segments. The
1444        //    calculated spread is the standard deviation of the lengths. Smaller is
1445        //    better (more uniform lengths of the route groups.)
1446        let mut best_circuit = Vec::new();
1447        let mut best_route_segments = Vec::new();
1448        let mut best_max_seg_len = 0;
1449        let mut best_min_spread = f32::MAX;
1450        let mut best_iteration = 0;
1451
1452        for i in 0..iterations {
1453            // Deterministically randomize the node order to search for the best route
1454            // segments.
1455            let mut eulerized_node_connections = mutable_node_connections.clone();
1456
1457            let mut odd_connected_count = odd_keys.len();
1458            assert!(
1459                odd_connected_count.is_multiple_of(2),
1460                "Odd connected count should be even, got {}",
1461                odd_connected_count
1462            );
1463            assert!(
1464                odd_keys.len()
1465                    == eulerized_node_connections
1466                        .iter()
1467                        .filter(|(_, node)| node.connected.len() % 2 == 1)
1468                        .count()
1469            );
1470
1471            // It's possible that the graphs starts with no odd nodes after removing edges
1472            // above.
1473            if odd_connected_count > 0 {
1474                odd_keys.shuffle(&mut rng);
1475
1476                // The edges to be added. An edge is a tuple of two node ids/indices.
1477                let mut edges_to_add = DHashSet::default();
1478                // Each odd node will be paired with only one other odd node.
1479                // Keep track of which nodes have been paired already.
1480                let mut paired_odd_nodes = DHashSet::default();
1481
1482                for node_key in odd_keys.iter() {
1483                    // Skip nodes that are already paired.
1484                    if paired_odd_nodes.contains(node_key) {
1485                        continue;
1486                    }
1487                    if let Some(node) = mutable_node_connections.get(node_key) {
1488                        // find the closest node other than nodes that are already connected to
1489                        // this one.
1490                        let mut closest_node_id = None;
1491                        let mut closest_distance = f64::MAX;
1492                        for candidate_key in odd_keys.iter() {
1493                            // Skip nodes that are already paired.
1494                            if paired_odd_nodes.contains(candidate_key) {
1495                                continue;
1496                            }
1497                            if let Some(candidate_node) =
1498                                mutable_node_connections.get(candidate_key)
1499                            {
1500                                // Skip the node itself and nodes that are already connected to this
1501                                // node.
1502                                if *candidate_key != *node_key
1503                                    && !node.connected.contains(candidate_key)
1504                                    && !candidate_node.connected.contains(node_key)
1505                                {
1506                                    // make sure the edge is specified in increasing node index
1507                                    // order to
1508                                    // avoid duplicates.
1509                                    let edge_to_add = if *node_key < *candidate_key {
1510                                        (*node_key, *candidate_key)
1511                                    } else {
1512                                        (*candidate_key, *node_key)
1513                                    };
1514                                    // Skip the edge if it is already in the edges_to_add set.
1515                                    if !edges_to_add.contains(&edge_to_add) {
1516                                        let pt1 = &all_dock_points[*node_key];
1517                                        let pt2 = &all_dock_points[*candidate_key];
1518                                        let v1 = Vec2 { x: pt1.x, y: pt1.y };
1519                                        let v2 = Vec2 { x: pt2.x, y: pt2.y };
1520                                        let distance = v1.distance_squared(v2);
1521                                        if distance < closest_distance {
1522                                            closest_distance = distance;
1523                                            closest_node_id = Some(*candidate_key);
1524                                        }
1525                                    }
1526                                }
1527                            }
1528                        }
1529                        // It's possible that the only odd nodes remaining are already connected to
1530                        // this node, but we still need to pair them. In
1531                        // this case, the connections become bidirectional,
1532                        // but that's okay for Eulerization and airships will still only follow each
1533                        // other in one direction.
1534                        if closest_node_id.is_none() {
1535                            // If no suitable node was found, repeat the search but allow
1536                            // connecting to nodes that are already connected to this one.
1537                            for candidate_key in odd_keys.iter() {
1538                                // Skip nodes that are already paired.
1539                                if paired_odd_nodes.contains(candidate_key) {
1540                                    continue;
1541                                }
1542                                // Skip the node itself
1543                                if *candidate_key != *node_key {
1544                                    // make sure the edge is specified in increasing node index
1545                                    // order to
1546                                    // avoid duplicates.
1547                                    let edge_to_add = if *node_key < *candidate_key {
1548                                        (*node_key, *candidate_key)
1549                                    } else {
1550                                        (*candidate_key, *node_key)
1551                                    };
1552                                    // Skip the edge if it is already in the edges_to_add set.
1553                                    if !edges_to_add.contains(&edge_to_add) {
1554                                        let pt1 = &all_dock_points[*node_key];
1555                                        let pt2 = &all_dock_points[*candidate_key];
1556                                        let v1 = Vec2 { x: pt1.x, y: pt1.y };
1557                                        let v2 = Vec2 { x: pt2.x, y: pt2.y };
1558                                        let distance = v1.distance_squared(v2);
1559                                        if distance < closest_distance {
1560                                            closest_distance = distance;
1561                                            closest_node_id = Some(*candidate_key);
1562                                        }
1563                                    }
1564                                }
1565                            }
1566                        }
1567                        // If a closest node was found that is not already paired, add the edge.
1568                        // Note that this should not fail since we are guaranteed to have
1569                        // an even number of odd nodes.
1570                        if let Some(close_node_id) = closest_node_id {
1571                            // add the edge between node_id and closest_node_id
1572                            let edge_to_add = if *node_key < close_node_id {
1573                                (*node_key, close_node_id)
1574                            } else {
1575                                (close_node_id, *node_key)
1576                            };
1577                            edges_to_add.insert(edge_to_add);
1578                            paired_odd_nodes.insert(*node_key);
1579                            paired_odd_nodes.insert(close_node_id);
1580                        } else {
1581                            error!("Cannot pair all odd nodes, this should not happen.");
1582                        }
1583                    }
1584                    if edges_to_add.len() == odd_connected_count / 2 {
1585                        // If we have paired all odd nodes, break out of the loop.
1586                        // The break is necessary because the outer loop iterates over
1587                        // all odd keys but we only need make 1/2 that many pairs of nodes.
1588                        break;
1589                    }
1590                }
1591                for edge in edges_to_add {
1592                    add_edge(edge, &mut eulerized_node_connections);
1593                }
1594                // count the number of nodes with an odd connected count
1595                odd_connected_count = eulerized_node_connections
1596                    .iter()
1597                    .filter(|(_, node)| node.connected.len() % 2 == 1)
1598                    .count();
1599
1600                #[cfg(debug_assertions)]
1601                {
1602                    let total_connections = eulerized_node_connections
1603                        .iter()
1604                        .map(|(_, node)| node.connected.len())
1605                        .sum::<usize>();
1606                    debug_airship_eulerization!(
1607                        "Outer Iteration: {}, After Adding, odd connected count: {} in {} nodes, \
1608                         total connections: {}",
1609                        i,
1610                        odd_connected_count,
1611                        eulerized_node_connections.len(),
1612                        total_connections
1613                    );
1614                }
1615            }
1616
1617            // If all nodes have an even number of edges, proceed with finding the best
1618            // Eulerian circuit for the given node configuration.
1619            if odd_connected_count == 0 {
1620                // Find the best Eulerian circuit for the current node connections
1621                if let Some((route_segments, circuit, max_seg_len, min_spread, _)) =
1622                    find_best_eulerian_circuit(&eulerized_node_connections)
1623                {
1624                    #[cfg(debug_assertions)]
1625                    {
1626                        debug_airship_eulerization!("Outer Iteration: {}", i);
1627                        debug_airship_eulerization!("Max segment length: {}", max_seg_len);
1628                        debug_airship_eulerization!("Min spread: {}", min_spread);
1629                        debug_airship_eulerization!("Segments count:");
1630                        route_segments.iter().enumerate().for_each(|segment| {
1631                            debug_airship_eulerization!("  {} : {}", segment.0, segment.1.len());
1632                        });
1633                    }
1634                    // A Eulerian circuit was found, apply the goal criteria to find the best
1635                    // circuit.
1636                    if max_seg_len > best_max_seg_len
1637                        || (max_seg_len == best_max_seg_len && min_spread < best_min_spread)
1638                    {
1639                        best_circuit = circuit;
1640                        best_route_segments = route_segments;
1641                        best_max_seg_len = max_seg_len;
1642                        best_min_spread = min_spread;
1643                        best_iteration = i;
1644                    }
1645                }
1646            } else {
1647                debug_airship_eulerization!(
1648                    "Error, this should not happen: iteration {}, odd connected count: {} of {} \
1649                     nodes, total connections: {}, SKIPPING iteration",
1650                    i,
1651                    odd_connected_count,
1652                    eulerized_node_connections.len(),
1653                    eulerized_node_connections
1654                        .iter()
1655                        .map(|(_, node)| node.connected.len())
1656                        .sum::<usize>()
1657                );
1658                error!(
1659                    "Eulerian circuit not found on iteration {}. Odd connected count is not zero, \
1660                     this should not happen",
1661                    i
1662                );
1663            }
1664        }
1665        #[cfg(debug_assertions)]
1666        {
1667            debug_airship_eulerization!("Max segment length: {}", best_max_seg_len);
1668            debug_airship_eulerization!("Min spread: {}", best_min_spread);
1669            debug_airship_eulerization!("Iteration: {}", best_iteration);
1670            debug_airship_eulerization!("Segments count:");
1671            best_route_segments.iter().enumerate().for_each(|segment| {
1672                debug_airship_eulerization!("  {} : {}", segment.0, segment.1.len());
1673            });
1674        }
1675
1676        if best_route_segments.is_empty() {
1677            return None;
1678        }
1679        Some((
1680            best_route_segments,
1681            best_circuit,
1682            best_max_seg_len,
1683            best_min_spread,
1684            best_iteration,
1685        ))
1686    }
1687}
1688
1689/// Find the best Eulerian circuit for the given graph of dock nodes.
1690/// Try each node as the starting point for a circuit.
1691/// The best circuit is the one with the longest routes (sub-segments
1692/// of the circuit), and where the route lengths are equal as possible.
1693fn find_best_eulerian_circuit(
1694    graph: &DockNodeGraph,
1695) -> Option<(Vec<Vec<usize>>, Vec<usize>, usize, f32, usize)> {
1696    let mut best_circuit = Vec::new();
1697    let mut best_route_segments = Vec::new();
1698    let mut best_max_seg_len = 0;
1699    let mut best_min_spread = f32::MAX;
1700    let mut best_iteration = 0;
1701
1702    let graph_keys = graph.keys().copied().collect::<Vec<_>>();
1703
1704    // Repeat for each node as the starting point.
1705    for (i, &start_vertex) in graph_keys.iter().enumerate() {
1706        let mut graph = graph.clone();
1707        let mut circuit = Vec::new();
1708        let mut stack = Vec::new();
1709        let mut circuit_nodes = DHashSet::default();
1710
1711        let mut current_vertex = start_vertex;
1712
1713        // The algorithm for finding a Eulerian Circuit (Hierholzer's algorithm).
1714        while !stack.is_empty() || !graph[&current_vertex].connected.is_empty() {
1715            if graph[&current_vertex].connected.is_empty() {
1716                circuit.push(current_vertex);
1717                circuit_nodes.insert(current_vertex);
1718                current_vertex = stack.pop()?;
1719            } else {
1720                stack.push(current_vertex);
1721                if let Some(&next_vertex) = graph
1722                    .get(&current_vertex)?
1723                    .connected
1724                    .iter()
1725                    .find(|&vertex| !circuit_nodes.contains(vertex))
1726                    .or(graph.get(&current_vertex)?.connected.first())
1727                {
1728                    remove_edge((current_vertex, next_vertex), &mut graph);
1729                    current_vertex = next_vertex;
1730                } else {
1731                    return None;
1732                }
1733            }
1734        }
1735        circuit.push(current_vertex);
1736        circuit.reverse();
1737
1738        if let Some((route_segments, max_seg_len, min_spread)) =
1739            best_eulerian_circuit_segments(&graph, &circuit)
1740            && (max_seg_len > best_max_seg_len
1741                || (max_seg_len == best_max_seg_len && min_spread < best_min_spread))
1742        {
1743            best_circuit = circuit.clone();
1744            best_route_segments = route_segments;
1745            best_max_seg_len = max_seg_len;
1746            best_min_spread = min_spread;
1747            best_iteration = i;
1748        }
1749    }
1750    if best_route_segments.is_empty() {
1751        return None;
1752    }
1753    Some((
1754        best_route_segments,
1755        best_circuit,
1756        best_max_seg_len,
1757        best_min_spread,
1758        best_iteration,
1759    ))
1760}
1761
1762/// Get the optimal grouping of Eulerian Circuit nodes and edges such that a
1763/// maximum number of sub-circuits are created, and the length of each
1764/// sub-circuit is as similar as possible.
1765///
1766/// The Airship dock nodes are connected in a Eulerian Circuit, where each edge
1767/// of the tessellation is traversed exactly once. The circuit is a closed loop,
1768/// so the first and last node are the same. The single circuit can be broken
1769/// into multiple segments, each being also a closed loop. This is desirable for
1770/// airship routes, to limit the number of airships associated with each "route"
1771/// where a route is a closed circuit of docking sites. Since each edge is flown
1772/// in only one direction, the maximum number of possible closed loop segments
1773/// is equal to the maximum number of edges connected to any node, divided by 2.
1774fn best_eulerian_circuit_segments(
1775    graph: &DockNodeGraph,
1776    circuit: &[usize],
1777) -> Option<(Vec<Vec<usize>>, usize, f32)> {
1778    // get the node_connections keys, which are node ids.
1779    // Sort the nodes (node ids) by the number of connections to other nodes.
1780    let sorted_node_ids: Vec<usize> = graph
1781        .keys()
1782        .copied()
1783        .sorted_by_key(|&node_id| graph[&node_id].connected.len())
1784        .rev()
1785        .collect();
1786
1787    let mut max_segments_count = 0;
1788    let mut min_segments_len_spread = f32::MAX;
1789    let mut best_segments = Vec::new();
1790
1791    // For each node_id in the sorted node ids,
1792    // break the circuit into circular segments that start and end with that
1793    // node_id. The best set of segments is the one with the most segments and
1794    // where the length of the segments differ the least.
1795    sorted_node_ids.iter().for_each(|&node_id| {
1796        let mut segments = Vec::new();
1797        let mut current_segment = Vec::new();
1798        let circuit_len = circuit.len();
1799        let mut starting_index = usize::MAX;
1800        let mut end_index = usize::MAX;
1801        let mut prev_value = usize::MAX;
1802
1803        for (index, &value) in circuit.iter().cycle().enumerate() {
1804            if value == node_id {
1805                if starting_index == usize::MAX {
1806                    starting_index = index;
1807                    if starting_index > 0 {
1808                        end_index = index + circuit_len - 1;
1809                    } else {
1810                        end_index = index + circuit_len - 2;
1811                    }
1812                }
1813                if !current_segment.is_empty() {
1814                    current_segment.push(value);
1815                    segments.push(current_segment);
1816                    current_segment = Vec::new();
1817                }
1818            }
1819            if starting_index < usize::MAX {
1820                if value != prev_value {
1821                    current_segment.push(value);
1822                }
1823                prev_value = value;
1824            }
1825
1826            // Stop cycling once we've looped back to the value before the starting index
1827            if index == end_index {
1828                break;
1829            }
1830        }
1831
1832        // Add the last segment
1833        if !current_segment.is_empty() {
1834            current_segment.push(node_id);
1835            segments.push(current_segment);
1836        }
1837
1838        let avg_segment_length = segments.iter().map(|segment| segment.len()).sum::<usize>() as f32
1839            / segments.len() as f32;
1840
1841        // We want similar segment lengths, so calculate the spread as the
1842        // standard deviation of the segment lengths.
1843        let seg_lengths_spread = segments
1844            .iter()
1845            .map(|segment| (segment.len() as f32 - avg_segment_length).powi(2))
1846            .sum::<f32>()
1847            .sqrt()
1848            / segments.len() as f32;
1849
1850        // First take the longest segment count, then if the segment count is the same
1851        // as the longest so far, take the one with the least length spread.
1852        if segments.len() > max_segments_count {
1853            max_segments_count = segments.len();
1854            min_segments_len_spread = seg_lengths_spread;
1855            best_segments = segments;
1856        } else if segments.len() == max_segments_count
1857            && seg_lengths_spread < min_segments_len_spread
1858        {
1859            min_segments_len_spread = seg_lengths_spread;
1860            best_segments = segments;
1861        }
1862    });
1863    if best_segments.is_empty() {
1864        return None;
1865    }
1866    Some((best_segments, max_segments_count, min_segments_len_spread))
1867}
1868
1869// ------------------------------------------------
1870// All code below this is for testing purposes only
1871// ------------------------------------------------
1872
1873// Public so it could be used in other modules' tests.
1874#[cfg(test)]
1875pub fn airships_from_test_data() -> Airships {
1876    let mut store = Store::<Site>::default();
1877    let dummy_site = Site::default();
1878    let dummy_site_id = store.insert(dummy_site);
1879
1880    let docks = vec![
1881        AirshipDockPositions {
1882            center: Vec2 {
1883                x: 26688.0,
1884                y: 4758.0,
1885            },
1886            docking_positions: vec![
1887                AirshipDockingPosition(0, Vec3 {
1888                    x: 26707.0,
1889                    y: 4758.0,
1890                    z: 213.0,
1891                }),
1892                AirshipDockingPosition(1, Vec3 {
1893                    x: 26688.0,
1894                    y: 4777.0,
1895                    z: 213.0,
1896                }),
1897                AirshipDockingPosition(2, Vec3 {
1898                    x: 26669.0,
1899                    y: 4758.0,
1900                    z: 213.0,
1901                }),
1902                AirshipDockingPosition(3, Vec3 {
1903                    x: 26688.0,
1904                    y: 4739.0,
1905                    z: 213.0,
1906                }),
1907            ],
1908            site_id: dummy_site_id,
1909        },
1910        AirshipDockPositions {
1911            center: Vec2 {
1912                x: 24574.0,
1913                y: 26108.0,
1914            },
1915            docking_positions: vec![
1916                AirshipDockingPosition(4, Vec3 {
1917                    x: 24593.0,
1918                    y: 26108.0,
1919                    z: 214.0,
1920                }),
1921                AirshipDockingPosition(5, Vec3 {
1922                    x: 24574.0,
1923                    y: 26127.0,
1924                    z: 214.0,
1925                }),
1926                AirshipDockingPosition(6, Vec3 {
1927                    x: 24555.0,
1928                    y: 26108.0,
1929                    z: 214.0,
1930                }),
1931                AirshipDockingPosition(7, Vec3 {
1932                    x: 24574.0,
1933                    y: 26089.0,
1934                    z: 214.0,
1935                }),
1936            ],
1937            site_id: dummy_site_id,
1938        },
1939        AirshipDockPositions {
1940            center: Vec2 {
1941                x: 24253.0,
1942                y: 20715.0,
1943            },
1944            docking_positions: vec![
1945                AirshipDockingPosition(8, Vec3 {
1946                    x: 24272.0,
1947                    y: 20715.0,
1948                    z: 515.0,
1949                }),
1950                AirshipDockingPosition(9, Vec3 {
1951                    x: 24253.0,
1952                    y: 20734.0,
1953                    z: 515.0,
1954                }),
1955                AirshipDockingPosition(10, Vec3 {
1956                    x: 24234.0,
1957                    y: 20715.0,
1958                    z: 515.0,
1959                }),
1960                AirshipDockingPosition(11, Vec3 {
1961                    x: 24253.0,
1962                    y: 20696.0,
1963                    z: 515.0,
1964                }),
1965            ],
1966            site_id: dummy_site_id,
1967        },
1968        AirshipDockPositions {
1969            center: Vec2 {
1970                x: 20809.0,
1971                y: 6555.0,
1972            },
1973            docking_positions: vec![
1974                AirshipDockingPosition(12, Vec3 {
1975                    x: 20828.0,
1976                    y: 6555.0,
1977                    z: 216.0,
1978                }),
1979                AirshipDockingPosition(13, Vec3 {
1980                    x: 20809.0,
1981                    y: 6574.0,
1982                    z: 216.0,
1983                }),
1984                AirshipDockingPosition(14, Vec3 {
1985                    x: 20790.0,
1986                    y: 6555.0,
1987                    z: 216.0,
1988                }),
1989                AirshipDockingPosition(15, Vec3 {
1990                    x: 20809.0,
1991                    y: 6536.0,
1992                    z: 216.0,
1993                }),
1994            ],
1995            site_id: dummy_site_id,
1996        },
1997        AirshipDockPositions {
1998            center: Vec2 {
1999                x: 16492.0,
2000                y: 1061.0,
2001            },
2002            docking_positions: vec![
2003                AirshipDockingPosition(16, Vec3 {
2004                    x: 16511.0,
2005                    y: 1061.0,
2006                    z: 211.0,
2007                }),
2008                AirshipDockingPosition(17, Vec3 {
2009                    x: 16492.0,
2010                    y: 1080.0,
2011                    z: 211.0,
2012                }),
2013                AirshipDockingPosition(18, Vec3 {
2014                    x: 16473.0,
2015                    y: 1061.0,
2016                    z: 211.0,
2017                }),
2018                AirshipDockingPosition(19, Vec3 {
2019                    x: 16492.0,
2020                    y: 1042.0,
2021                    z: 211.0,
2022                }),
2023            ],
2024            site_id: dummy_site_id,
2025        },
2026        AirshipDockPositions {
2027            center: Vec2 {
2028                x: 18452.0,
2029                y: 11236.0,
2030            },
2031            docking_positions: vec![
2032                AirshipDockingPosition(20, Vec3 {
2033                    x: 18471.0,
2034                    y: 11236.0,
2035                    z: 421.0,
2036                }),
2037                AirshipDockingPosition(21, Vec3 {
2038                    x: 18452.0,
2039                    y: 11255.0,
2040                    z: 421.0,
2041                }),
2042                AirshipDockingPosition(22, Vec3 {
2043                    x: 18433.0,
2044                    y: 11236.0,
2045                    z: 421.0,
2046                }),
2047                AirshipDockingPosition(23, Vec3 {
2048                    x: 18452.0,
2049                    y: 11217.0,
2050                    z: 421.0,
2051                }),
2052            ],
2053            site_id: dummy_site_id,
2054        },
2055        AirshipDockPositions {
2056            center: Vec2 {
2057                x: 21870.0,
2058                y: 8530.0,
2059            },
2060            docking_positions: vec![
2061                AirshipDockingPosition(24, Vec3 {
2062                    x: 21889.0,
2063                    y: 8530.0,
2064                    z: 216.0,
2065                }),
2066                AirshipDockingPosition(25, Vec3 {
2067                    x: 21870.0,
2068                    y: 8549.0,
2069                    z: 216.0,
2070                }),
2071                AirshipDockingPosition(26, Vec3 {
2072                    x: 21851.0,
2073                    y: 8530.0,
2074                    z: 216.0,
2075                }),
2076                AirshipDockingPosition(27, Vec3 {
2077                    x: 21870.0,
2078                    y: 8511.0,
2079                    z: 216.0,
2080                }),
2081            ],
2082            site_id: dummy_site_id,
2083        },
2084        AirshipDockPositions {
2085            center: Vec2 {
2086                x: 22577.0,
2087                y: 15197.0,
2088            },
2089            docking_positions: vec![
2090                AirshipDockingPosition(28, Vec3 {
2091                    x: 22605.0,
2092                    y: 15197.0,
2093                    z: 277.0,
2094                }),
2095                AirshipDockingPosition(29, Vec3 {
2096                    x: 22577.0,
2097                    y: 15225.0,
2098                    z: 277.0,
2099                }),
2100                AirshipDockingPosition(30, Vec3 {
2101                    x: 22549.0,
2102                    y: 15197.0,
2103                    z: 277.0,
2104                }),
2105                AirshipDockingPosition(31, Vec3 {
2106                    x: 22577.0,
2107                    y: 15169.0,
2108                    z: 277.0,
2109                }),
2110            ],
2111            site_id: dummy_site_id,
2112        },
2113        AirshipDockPositions {
2114            center: Vec2 {
2115                x: 5477.0,
2116                y: 15207.0,
2117            },
2118            docking_positions: vec![
2119                AirshipDockingPosition(32, Vec3 {
2120                    x: 5514.0,
2121                    y: 15207.0,
2122                    z: 1675.0,
2123                }),
2124                AirshipDockingPosition(33, Vec3 {
2125                    x: 5477.0,
2126                    y: 15244.0,
2127                    z: 1675.0,
2128                }),
2129                AirshipDockingPosition(34, Vec3 {
2130                    x: 5440.0,
2131                    y: 15207.0,
2132                    z: 1675.0,
2133                }),
2134                AirshipDockingPosition(35, Vec3 {
2135                    x: 5477.0,
2136                    y: 15170.0,
2137                    z: 1675.0,
2138                }),
2139            ],
2140            site_id: dummy_site_id,
2141        },
2142        AirshipDockPositions {
2143            center: Vec2 {
2144                x: 23884.0,
2145                y: 24302.0,
2146            },
2147            docking_positions: vec![
2148                AirshipDockingPosition(36, Vec3 {
2149                    x: 23903.0,
2150                    y: 24302.0,
2151                    z: 214.0,
2152                }),
2153                AirshipDockingPosition(37, Vec3 {
2154                    x: 23884.0,
2155                    y: 24321.0,
2156                    z: 214.0,
2157                }),
2158                AirshipDockingPosition(38, Vec3 {
2159                    x: 23865.0,
2160                    y: 24302.0,
2161                    z: 214.0,
2162                }),
2163                AirshipDockingPosition(39, Vec3 {
2164                    x: 23884.0,
2165                    y: 24283.0,
2166                    z: 214.0,
2167                }),
2168            ],
2169            site_id: dummy_site_id,
2170        },
2171        AirshipDockPositions {
2172            center: Vec2 {
2173                x: 13373.0,
2174                y: 2313.0,
2175            },
2176            docking_positions: vec![
2177                AirshipDockingPosition(40, Vec3 {
2178                    x: 13392.0,
2179                    y: 2313.0,
2180                    z: 259.0,
2181                }),
2182                AirshipDockingPosition(41, Vec3 {
2183                    x: 13373.0,
2184                    y: 2332.0,
2185                    z: 259.0,
2186                }),
2187                AirshipDockingPosition(42, Vec3 {
2188                    x: 13354.0,
2189                    y: 2313.0,
2190                    z: 259.0,
2191                }),
2192                AirshipDockingPosition(43, Vec3 {
2193                    x: 13373.0,
2194                    y: 2294.0,
2195                    z: 259.0,
2196                }),
2197            ],
2198            site_id: dummy_site_id,
2199        },
2200        AirshipDockPositions {
2201            center: Vec2 {
2202                x: 20141.0,
2203                y: 31861.0,
2204            },
2205            docking_positions: vec![
2206                AirshipDockingPosition(44, Vec3 {
2207                    x: 20160.0,
2208                    y: 31861.0,
2209                    z: 215.0,
2210                }),
2211                AirshipDockingPosition(45, Vec3 {
2212                    x: 20141.0,
2213                    y: 31880.0,
2214                    z: 215.0,
2215                }),
2216                AirshipDockingPosition(46, Vec3 {
2217                    x: 20122.0,
2218                    y: 31861.0,
2219                    z: 215.0,
2220                }),
2221                AirshipDockingPosition(47, Vec3 {
2222                    x: 20141.0,
2223                    y: 31842.0,
2224                    z: 215.0,
2225                }),
2226            ],
2227            site_id: dummy_site_id,
2228        },
2229        AirshipDockPositions {
2230            center: Vec2 {
2231                x: 29713.0,
2232                y: 24533.0,
2233            },
2234            docking_positions: vec![
2235                AirshipDockingPosition(48, Vec3 {
2236                    x: 29732.0,
2237                    y: 24533.0,
2238                    z: 214.0,
2239                }),
2240                AirshipDockingPosition(49, Vec3 {
2241                    x: 29713.0,
2242                    y: 24552.0,
2243                    z: 214.0,
2244                }),
2245                AirshipDockingPosition(50, Vec3 {
2246                    x: 29694.0,
2247                    y: 24533.0,
2248                    z: 214.0,
2249                }),
2250                AirshipDockingPosition(51, Vec3 {
2251                    x: 29713.0,
2252                    y: 24514.0,
2253                    z: 214.0,
2254                }),
2255            ],
2256            site_id: dummy_site_id,
2257        },
2258        AirshipDockPositions {
2259            center: Vec2 {
2260                x: 18992.0,
2261                y: 17120.0,
2262            },
2263            docking_positions: vec![
2264                AirshipDockingPosition(52, Vec3 {
2265                    x: 19011.0,
2266                    y: 17120.0,
2267                    z: 435.0,
2268                }),
2269                AirshipDockingPosition(53, Vec3 {
2270                    x: 18992.0,
2271                    y: 17139.0,
2272                    z: 435.0,
2273                }),
2274                AirshipDockingPosition(54, Vec3 {
2275                    x: 18973.0,
2276                    y: 17120.0,
2277                    z: 435.0,
2278                }),
2279                AirshipDockingPosition(55, Vec3 {
2280                    x: 18992.0,
2281                    y: 17101.0,
2282                    z: 435.0,
2283                }),
2284            ],
2285            site_id: dummy_site_id,
2286        },
2287        AirshipDockPositions {
2288            center: Vec2 {
2289                x: 7705.0,
2290                y: 12533.0,
2291            },
2292            docking_positions: vec![
2293                AirshipDockingPosition(56, Vec3 {
2294                    x: 7742.0,
2295                    y: 12533.0,
2296                    z: 1911.0,
2297                }),
2298                AirshipDockingPosition(57, Vec3 {
2299                    x: 7705.0,
2300                    y: 12570.0,
2301                    z: 1911.0,
2302                }),
2303                AirshipDockingPosition(58, Vec3 {
2304                    x: 7668.0,
2305                    y: 12533.0,
2306                    z: 1911.0,
2307                }),
2308                AirshipDockingPosition(59, Vec3 {
2309                    x: 7705.0,
2310                    y: 12496.0,
2311                    z: 1911.0,
2312                }),
2313            ],
2314            site_id: dummy_site_id,
2315        },
2316        AirshipDockPositions {
2317            center: Vec2 {
2318                x: 30365.0,
2319                y: 12987.0,
2320            },
2321            docking_positions: vec![
2322                AirshipDockingPosition(60, Vec3 {
2323                    x: 30393.0,
2324                    y: 12987.0,
2325                    z: 244.0,
2326                }),
2327                AirshipDockingPosition(61, Vec3 {
2328                    x: 30365.0,
2329                    y: 13015.0,
2330                    z: 244.0,
2331                }),
2332                AirshipDockingPosition(62, Vec3 {
2333                    x: 30337.0,
2334                    y: 12987.0,
2335                    z: 244.0,
2336                }),
2337                AirshipDockingPosition(63, Vec3 {
2338                    x: 30365.0,
2339                    y: 12959.0,
2340                    z: 244.0,
2341                }),
2342            ],
2343            site_id: dummy_site_id,
2344        },
2345        AirshipDockPositions {
2346            center: Vec2 {
2347                x: 10142.0,
2348                y: 19190.0,
2349            },
2350            docking_positions: vec![
2351                AirshipDockingPosition(64, Vec3 {
2352                    x: 10170.0,
2353                    y: 19190.0,
2354                    z: 1141.0,
2355                }),
2356                AirshipDockingPosition(65, Vec3 {
2357                    x: 10142.0,
2358                    y: 19218.0,
2359                    z: 1141.0,
2360                }),
2361                AirshipDockingPosition(66, Vec3 {
2362                    x: 10114.0,
2363                    y: 19190.0,
2364                    z: 1141.0,
2365                }),
2366                AirshipDockingPosition(67, Vec3 {
2367                    x: 10142.0,
2368                    y: 19162.0,
2369                    z: 1141.0,
2370                }),
2371            ],
2372            site_id: dummy_site_id,
2373        },
2374        AirshipDockPositions {
2375            center: Vec2 {
2376                x: 13716.0,
2377                y: 17505.0,
2378            },
2379            docking_positions: vec![
2380                AirshipDockingPosition(68, Vec3 {
2381                    x: 13753.0,
2382                    y: 17505.0,
2383                    z: 1420.0,
2384                }),
2385                AirshipDockingPosition(69, Vec3 {
2386                    x: 13716.0,
2387                    y: 17542.0,
2388                    z: 1420.0,
2389                }),
2390                AirshipDockingPosition(70, Vec3 {
2391                    x: 13679.0,
2392                    y: 17505.0,
2393                    z: 1420.0,
2394                }),
2395                AirshipDockingPosition(71, Vec3 {
2396                    x: 13716.0,
2397                    y: 17468.0,
2398                    z: 1420.0,
2399                }),
2400            ],
2401            site_id: dummy_site_id,
2402        },
2403        AirshipDockPositions {
2404            center: Vec2 {
2405                x: 9383.0,
2406                y: 17145.0,
2407            },
2408            docking_positions: vec![
2409                AirshipDockingPosition(72, Vec3 {
2410                    x: 9411.0,
2411                    y: 17145.0,
2412                    z: 909.0,
2413                }),
2414                AirshipDockingPosition(73, Vec3 {
2415                    x: 9383.0,
2416                    y: 17173.0,
2417                    z: 909.0,
2418                }),
2419                AirshipDockingPosition(74, Vec3 {
2420                    x: 9355.0,
2421                    y: 17145.0,
2422                    z: 909.0,
2423                }),
2424                AirshipDockingPosition(75, Vec3 {
2425                    x: 9383.0,
2426                    y: 17117.0,
2427                    z: 909.0,
2428                }),
2429            ],
2430            site_id: dummy_site_id,
2431        },
2432        AirshipDockPositions {
2433            center: Vec2 {
2434                x: 24424.0,
2435                y: 7800.0,
2436            },
2437            docking_positions: vec![
2438                AirshipDockingPosition(76, Vec3 {
2439                    x: 24443.0,
2440                    y: 7800.0,
2441                    z: 329.0,
2442                }),
2443                AirshipDockingPosition(77, Vec3 {
2444                    x: 24424.0,
2445                    y: 7819.0,
2446                    z: 329.0,
2447                }),
2448                AirshipDockingPosition(78, Vec3 {
2449                    x: 24405.0,
2450                    y: 7800.0,
2451                    z: 329.0,
2452                }),
2453                AirshipDockingPosition(79, Vec3 {
2454                    x: 24424.0,
2455                    y: 7781.0,
2456                    z: 329.0,
2457                }),
2458            ],
2459            site_id: dummy_site_id,
2460        },
2461        AirshipDockPositions {
2462            center: Vec2 {
2463                x: 7528.0,
2464                y: 28426.0,
2465            },
2466            docking_positions: vec![
2467                AirshipDockingPosition(80, Vec3 {
2468                    x: 7547.0,
2469                    y: 28426.0,
2470                    z: 218.0,
2471                }),
2472                AirshipDockingPosition(81, Vec3 {
2473                    x: 7528.0,
2474                    y: 28445.0,
2475                    z: 218.0,
2476                }),
2477                AirshipDockingPosition(82, Vec3 {
2478                    x: 7509.0,
2479                    y: 28426.0,
2480                    z: 218.0,
2481                }),
2482                AirshipDockingPosition(83, Vec3 {
2483                    x: 7528.0,
2484                    y: 28407.0,
2485                    z: 218.0,
2486                }),
2487            ],
2488            site_id: dummy_site_id,
2489        },
2490        AirshipDockPositions {
2491            center: Vec2 {
2492                x: 9942.0,
2493                y: 30936.0,
2494            },
2495            docking_positions: vec![
2496                AirshipDockingPosition(84, Vec3 {
2497                    x: 9961.0,
2498                    y: 30936.0,
2499                    z: 185.0,
2500                }),
2501                AirshipDockingPosition(85, Vec3 {
2502                    x: 9942.0,
2503                    y: 30955.0,
2504                    z: 185.0,
2505                }),
2506                AirshipDockingPosition(86, Vec3 {
2507                    x: 9923.0,
2508                    y: 30936.0,
2509                    z: 185.0,
2510                }),
2511                AirshipDockingPosition(87, Vec3 {
2512                    x: 9942.0,
2513                    y: 30917.0,
2514                    z: 185.0,
2515                }),
2516            ],
2517            site_id: dummy_site_id,
2518        },
2519        AirshipDockPositions {
2520            center: Vec2 {
2521                x: 27915.0,
2522                y: 18559.0,
2523            },
2524            docking_positions: vec![
2525                AirshipDockingPosition(88, Vec3 {
2526                    x: 27934.0,
2527                    y: 18559.0,
2528                    z: 498.0,
2529                }),
2530                AirshipDockingPosition(89, Vec3 {
2531                    x: 27915.0,
2532                    y: 18578.0,
2533                    z: 498.0,
2534                }),
2535                AirshipDockingPosition(90, Vec3 {
2536                    x: 27896.0,
2537                    y: 18559.0,
2538                    z: 498.0,
2539                }),
2540                AirshipDockingPosition(91, Vec3 {
2541                    x: 27915.0,
2542                    y: 18540.0,
2543                    z: 498.0,
2544                }),
2545            ],
2546            site_id: dummy_site_id,
2547        },
2548        AirshipDockPositions {
2549            center: Vec2 {
2550                x: 3688.0,
2551                y: 29168.0,
2552            },
2553            docking_positions: vec![
2554                AirshipDockingPosition(92, Vec3 {
2555                    x: 3711.0,
2556                    y: 29168.0,
2557                    z: 198.0,
2558                }),
2559                AirshipDockingPosition(93, Vec3 {
2560                    x: 3688.0,
2561                    y: 29191.0,
2562                    z: 198.0,
2563                }),
2564                AirshipDockingPosition(94, Vec3 {
2565                    x: 3665.0,
2566                    y: 29168.0,
2567                    z: 198.0,
2568                }),
2569                AirshipDockingPosition(95, Vec3 {
2570                    x: 3688.0,
2571                    y: 29145.0,
2572                    z: 198.0,
2573                }),
2574            ],
2575            site_id: dummy_site_id,
2576        },
2577        AirshipDockPositions {
2578            center: Vec2 {
2579                x: 15864.0,
2580                y: 15584.0,
2581            },
2582            docking_positions: vec![
2583                AirshipDockingPosition(96, Vec3 {
2584                    x: 15892.0,
2585                    y: 15584.0,
2586                    z: 419.0,
2587                }),
2588                AirshipDockingPosition(97, Vec3 {
2589                    x: 15864.0,
2590                    y: 15612.0,
2591                    z: 419.0,
2592                }),
2593                AirshipDockingPosition(98, Vec3 {
2594                    x: 15836.0,
2595                    y: 15584.0,
2596                    z: 419.0,
2597                }),
2598                AirshipDockingPosition(99, Vec3 {
2599                    x: 15864.0,
2600                    y: 15556.0,
2601                    z: 419.0,
2602                }),
2603            ],
2604            site_id: dummy_site_id,
2605        },
2606        AirshipDockPositions {
2607            center: Vec2 {
2608                x: 9975.0,
2609                y: 24289.0,
2610            },
2611            docking_positions: vec![
2612                AirshipDockingPosition(100, Vec3 {
2613                    x: 10012.0,
2614                    y: 24289.0,
2615                    z: 755.0,
2616                }),
2617                AirshipDockingPosition(101, Vec3 {
2618                    x: 9975.0,
2619                    y: 24326.0,
2620                    z: 755.0,
2621                }),
2622                AirshipDockingPosition(102, Vec3 {
2623                    x: 9938.0,
2624                    y: 24289.0,
2625                    z: 755.0,
2626                }),
2627                AirshipDockingPosition(103, Vec3 {
2628                    x: 9975.0,
2629                    y: 24252.0,
2630                    z: 755.0,
2631                }),
2632            ],
2633            site_id: dummy_site_id,
2634        },
2635        AirshipDockPositions {
2636            center: Vec2 {
2637                x: 479.0,
2638                y: 18279.0,
2639            },
2640            docking_positions: vec![
2641                AirshipDockingPosition(104, Vec3 {
2642                    x: 516.0,
2643                    y: 18279.0,
2644                    z: 449.0,
2645                }),
2646                AirshipDockingPosition(105, Vec3 {
2647                    x: 479.0,
2648                    y: 18316.0,
2649                    z: 449.0,
2650                }),
2651                AirshipDockingPosition(106, Vec3 {
2652                    x: 442.0,
2653                    y: 18279.0,
2654                    z: 449.0,
2655                }),
2656                AirshipDockingPosition(107, Vec3 {
2657                    x: 479.0,
2658                    y: 18242.0,
2659                    z: 449.0,
2660                }),
2661            ],
2662            site_id: dummy_site_id,
2663        },
2664        AirshipDockPositions {
2665            center: Vec2 {
2666                x: 26543.0,
2667                y: 17175.0,
2668            },
2669            docking_positions: vec![
2670                AirshipDockingPosition(108, Vec3 {
2671                    x: 26566.0,
2672                    y: 17175.0,
2673                    z: 362.0,
2674                }),
2675                AirshipDockingPosition(109, Vec3 {
2676                    x: 26543.0,
2677                    y: 17198.0,
2678                    z: 362.0,
2679                }),
2680                AirshipDockingPosition(110, Vec3 {
2681                    x: 26520.0,
2682                    y: 17175.0,
2683                    z: 362.0,
2684                }),
2685                AirshipDockingPosition(111, Vec3 {
2686                    x: 26543.0,
2687                    y: 17152.0,
2688                    z: 362.0,
2689                }),
2690            ],
2691            site_id: dummy_site_id,
2692        },
2693    ];
2694
2695    let routes = vec![
2696        vec![
2697            AirshipRouteLeg {
2698                dest_index: 13,
2699                platform: AirshipDockPlatform::SouthPlatform,
2700            },
2701            AirshipRouteLeg {
2702                dest_index: 24,
2703                platform: AirshipDockPlatform::EastPlatform,
2704            },
2705            AirshipRouteLeg {
2706                dest_index: 17,
2707                platform: AirshipDockPlatform::EastPlatform,
2708            },
2709            AirshipRouteLeg {
2710                dest_index: 13,
2711                platform: AirshipDockPlatform::WestPlatform,
2712            },
2713            AirshipRouteLeg {
2714                dest_index: 9,
2715                platform: AirshipDockPlatform::SouthPlatform,
2716            },
2717            AirshipRouteLeg {
2718                dest_index: 2,
2719                platform: AirshipDockPlatform::NorthPlatform,
2720            },
2721            AirshipRouteLeg {
2722                dest_index: 13,
2723                platform: AirshipDockPlatform::EastPlatform,
2724            },
2725            AirshipRouteLeg {
2726                dest_index: 7,
2727                platform: AirshipDockPlatform::WestPlatform,
2728            },
2729            AirshipRouteLeg {
2730                dest_index: 2,
2731                platform: AirshipDockPlatform::SouthPlatform,
2732            },
2733            AirshipRouteLeg {
2734                dest_index: 22,
2735                platform: AirshipDockPlatform::WestPlatform,
2736            },
2737            AirshipRouteLeg {
2738                dest_index: 27,
2739                platform: AirshipDockPlatform::NorthPlatform,
2740            },
2741            AirshipRouteLeg {
2742                dest_index: 2,
2743                platform: AirshipDockPlatform::EastPlatform,
2744            },
2745            AirshipRouteLeg {
2746                dest_index: 12,
2747                platform: AirshipDockPlatform::WestPlatform,
2748            },
2749            AirshipRouteLeg {
2750                dest_index: 22,
2751                platform: AirshipDockPlatform::NorthPlatform,
2752            },
2753            AirshipRouteLeg {
2754                dest_index: 15,
2755                platform: AirshipDockPlatform::NorthPlatform,
2756            },
2757            AirshipRouteLeg {
2758                dest_index: 19,
2759                platform: AirshipDockPlatform::EastPlatform,
2760            },
2761            AirshipRouteLeg {
2762                dest_index: 6,
2763                platform: AirshipDockPlatform::EastPlatform,
2764            },
2765            AirshipRouteLeg {
2766                dest_index: 5,
2767                platform: AirshipDockPlatform::EastPlatform,
2768            },
2769        ],
2770        vec![
2771            AirshipRouteLeg {
2772                dest_index: 24,
2773                platform: AirshipDockPlatform::SouthPlatform,
2774            },
2775            AirshipRouteLeg {
2776                dest_index: 14,
2777                platform: AirshipDockPlatform::EastPlatform,
2778            },
2779            AirshipRouteLeg {
2780                dest_index: 18,
2781                platform: AirshipDockPlatform::SouthPlatform,
2782            },
2783            AirshipRouteLeg {
2784                dest_index: 16,
2785                platform: AirshipDockPlatform::SouthPlatform,
2786            },
2787            AirshipRouteLeg {
2788                dest_index: 17,
2789                platform: AirshipDockPlatform::WestPlatform,
2790            },
2791            AirshipRouteLeg {
2792                dest_index: 18,
2793                platform: AirshipDockPlatform::EastPlatform,
2794            },
2795            AirshipRouteLeg {
2796                dest_index: 8,
2797                platform: AirshipDockPlatform::EastPlatform,
2798            },
2799            AirshipRouteLeg {
2800                dest_index: 16,
2801                platform: AirshipDockPlatform::WestPlatform,
2802            },
2803            AirshipRouteLeg {
2804                dest_index: 26,
2805                platform: AirshipDockPlatform::EastPlatform,
2806            },
2807            AirshipRouteLeg {
2808                dest_index: 25,
2809                platform: AirshipDockPlatform::WestPlatform,
2810            },
2811            AirshipRouteLeg {
2812                dest_index: 13,
2813                platform: AirshipDockPlatform::NorthPlatform,
2814            },
2815            AirshipRouteLeg {
2816                dest_index: 11,
2817                platform: AirshipDockPlatform::SouthPlatform,
2818            },
2819            AirshipRouteLeg {
2820                dest_index: 1,
2821                platform: AirshipDockPlatform::NorthPlatform,
2822            },
2823            AirshipRouteLeg {
2824                dest_index: 9,
2825                platform: AirshipDockPlatform::NorthPlatform,
2826            },
2827            AirshipRouteLeg {
2828                dest_index: 25,
2829                platform: AirshipDockPlatform::EastPlatform,
2830            },
2831            AirshipRouteLeg {
2832                dest_index: 17,
2833                platform: AirshipDockPlatform::NorthPlatform,
2834            },
2835            AirshipRouteLeg {
2836                dest_index: 14,
2837                platform: AirshipDockPlatform::NorthPlatform,
2838            },
2839            AirshipRouteLeg {
2840                dest_index: 5,
2841                platform: AirshipDockPlatform::WestPlatform,
2842            },
2843        ],
2844        vec![
2845            AirshipRouteLeg {
2846                dest_index: 10,
2847                platform: AirshipDockPlatform::NorthPlatform,
2848            },
2849            AirshipRouteLeg {
2850                dest_index: 14,
2851                platform: AirshipDockPlatform::SouthPlatform,
2852            },
2853            AirshipRouteLeg {
2854                dest_index: 8,
2855                platform: AirshipDockPlatform::SouthPlatform,
2856            },
2857            AirshipRouteLeg {
2858                dest_index: 26,
2859                platform: AirshipDockPlatform::SouthPlatform,
2860            },
2861            AirshipRouteLeg {
2862                dest_index: 14,
2863                platform: AirshipDockPlatform::WestPlatform,
2864            },
2865            AirshipRouteLeg {
2866                dest_index: 16,
2867                platform: AirshipDockPlatform::EastPlatform,
2868            },
2869            AirshipRouteLeg {
2870                dest_index: 25,
2871                platform: AirshipDockPlatform::SouthPlatform,
2872            },
2873            AirshipRouteLeg {
2874                dest_index: 23,
2875                platform: AirshipDockPlatform::EastPlatform,
2876            },
2877            AirshipRouteLeg {
2878                dest_index: 20,
2879                platform: AirshipDockPlatform::WestPlatform,
2880            },
2881            AirshipRouteLeg {
2882                dest_index: 21,
2883                platform: AirshipDockPlatform::SouthPlatform,
2884            },
2885            AirshipRouteLeg {
2886                dest_index: 11,
2887                platform: AirshipDockPlatform::WestPlatform,
2888            },
2889            AirshipRouteLeg {
2890                dest_index: 9,
2891                platform: AirshipDockPlatform::WestPlatform,
2892            },
2893            AirshipRouteLeg {
2894                dest_index: 12,
2895                platform: AirshipDockPlatform::SouthPlatform,
2896            },
2897            AirshipRouteLeg {
2898                dest_index: 1,
2899                platform: AirshipDockPlatform::EastPlatform,
2900            },
2901            AirshipRouteLeg {
2902                dest_index: 7,
2903                platform: AirshipDockPlatform::NorthPlatform,
2904            },
2905            AirshipRouteLeg {
2906                dest_index: 27,
2907                platform: AirshipDockPlatform::WestPlatform,
2908            },
2909            AirshipRouteLeg {
2910                dest_index: 15,
2911                platform: AirshipDockPlatform::WestPlatform,
2912            },
2913            AirshipRouteLeg {
2914                dest_index: 7,
2915                platform: AirshipDockPlatform::EastPlatform,
2916            },
2917            AirshipRouteLeg {
2918                dest_index: 19,
2919                platform: AirshipDockPlatform::NorthPlatform,
2920            },
2921            AirshipRouteLeg {
2922                dest_index: 3,
2923                platform: AirshipDockPlatform::EastPlatform,
2924            },
2925            AirshipRouteLeg {
2926                dest_index: 5,
2927                platform: AirshipDockPlatform::SouthPlatform,
2928            },
2929        ],
2930        vec![
2931            AirshipRouteLeg {
2932                dest_index: 4,
2933                platform: AirshipDockPlatform::NorthPlatform,
2934            },
2935            AirshipRouteLeg {
2936                dest_index: 10,
2937                platform: AirshipDockPlatform::EastPlatform,
2938            },
2939            AirshipRouteLeg {
2940                dest_index: 3,
2941                platform: AirshipDockPlatform::WestPlatform,
2942            },
2943            AirshipRouteLeg {
2944                dest_index: 4,
2945                platform: AirshipDockPlatform::EastPlatform,
2946            },
2947            AirshipRouteLeg {
2948                dest_index: 0,
2949                platform: AirshipDockPlatform::WestPlatform,
2950            },
2951            AirshipRouteLeg {
2952                dest_index: 15,
2953                platform: AirshipDockPlatform::SouthPlatform,
2954            },
2955            AirshipRouteLeg {
2956                dest_index: 12,
2957                platform: AirshipDockPlatform::EastPlatform,
2958            },
2959            AirshipRouteLeg {
2960                dest_index: 11,
2961                platform: AirshipDockPlatform::EastPlatform,
2962            },
2963            AirshipRouteLeg {
2964                dest_index: 25,
2965                platform: AirshipDockPlatform::NorthPlatform,
2966            },
2967            AirshipRouteLeg {
2968                dest_index: 21,
2969                platform: AirshipDockPlatform::EastPlatform,
2970            },
2971            AirshipRouteLeg {
2972                dest_index: 23,
2973                platform: AirshipDockPlatform::NorthPlatform,
2974            },
2975            AirshipRouteLeg {
2976                dest_index: 26,
2977                platform: AirshipDockPlatform::NorthPlatform,
2978            },
2979            AirshipRouteLeg {
2980                dest_index: 10,
2981                platform: AirshipDockPlatform::WestPlatform,
2982            },
2983            AirshipRouteLeg {
2984                dest_index: 19,
2985                platform: AirshipDockPlatform::WestPlatform,
2986            },
2987            AirshipRouteLeg {
2988                dest_index: 0,
2989                platform: AirshipDockPlatform::NorthPlatform,
2990            },
2991            AirshipRouteLeg {
2992                dest_index: 3,
2993                platform: AirshipDockPlatform::SouthPlatform,
2994            },
2995            AirshipRouteLeg {
2996                dest_index: 6,
2997                platform: AirshipDockPlatform::SouthPlatform,
2998            },
2999            AirshipRouteLeg {
3000                dest_index: 7,
3001                platform: AirshipDockPlatform::SouthPlatform,
3002            },
3003            AirshipRouteLeg {
3004                dest_index: 5,
3005                platform: AirshipDockPlatform::NorthPlatform,
3006            },
3007        ],
3008    ];
3009
3010    Airships {
3011        airship_docks: docks,
3012        routes,
3013        ..Default::default()
3014    }
3015}
3016
3017#[cfg(test)]
3018mod tests {
3019    use super::{
3020        AirshipDockPlatform, AirshipDockingSide, Airships, DockNode, TriangulationExt,
3021        airships_from_test_data, approx::assert_relative_eq, find_best_eulerian_circuit,
3022        remove_edge,
3023    };
3024    use crate::util::{DHashMap, DHashSet};
3025    use delaunator::{Point, triangulate};
3026    use vek::{Quaternion, Vec2, Vec3};
3027
3028    #[test]
3029    fn basic_vec_test() {
3030        let vec1 = Vec3::new(0.0f32, 10.0, 0.0);
3031        let vec2 = Vec3::new(10.0, 0.0, 0.0);
3032        let a12 = vec2.angle_between(vec1);
3033        assert_relative_eq!(a12, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
3034
3035        let rotc2 = Quaternion::rotation_z(a12);
3036        let vec3 = rotc2 * vec2;
3037        assert!(vec3 == vec1);
3038    }
3039
3040    #[test]
3041    fn std_vec_angles_test() {
3042        let refvec = Vec2::new(0.0f32, 10.0);
3043
3044        let vec1 = Vec2::new(0.0f32, 10.0);
3045        let vec2 = Vec2::new(10.0f32, 0.0);
3046        let vec3 = Vec2::new(0.0f32, -10.0);
3047        let vec4 = Vec2::new(-10.0f32, 0.0);
3048
3049        let a1r = vec1.angle_between(refvec);
3050        assert!(a1r == 0.0f32);
3051
3052        let a2r = vec2.angle_between(refvec);
3053        assert_relative_eq!(a2r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
3054
3055        let a3r: f32 = vec3.angle_between(refvec);
3056        assert_relative_eq!(a3r, std::f32::consts::PI, epsilon = 0.00001);
3057
3058        let a4r = vec4.angle_between(refvec);
3059        assert_relative_eq!(a4r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
3060    }
3061
3062    #[test]
3063    fn vec_angles_test() {
3064        let refvec = Vec3::new(0.0f32, 10.0, 0.0);
3065
3066        let vec1 = Vec3::new(0.0f32, 10.0, 0.0);
3067        let vec2 = Vec3::new(10.0f32, 0.0, 0.0);
3068        let vec3 = Vec3::new(0.0f32, -10.0, 0.0);
3069        let vec4 = Vec3::new(-10.0f32, 0.0, 0.0);
3070
3071        let a1r = vec1.angle_between(refvec);
3072        let a1r3 = Airships::angle_between_vectors_ccw(vec1.xy(), refvec.xy());
3073        assert!(a1r == 0.0f32);
3074        assert!(a1r3 == 0.0f32);
3075
3076        let a2r = vec2.angle_between(refvec);
3077        let a2r3 = Airships::angle_between_vectors_ccw(vec2.xy(), refvec.xy());
3078        assert_relative_eq!(a2r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
3079        assert_relative_eq!(a2r3, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
3080
3081        let a3r: f32 = vec3.angle_between(refvec);
3082        let a3r3 = Airships::angle_between_vectors_ccw(vec3.xy(), refvec.xy());
3083        assert_relative_eq!(a3r, std::f32::consts::PI, epsilon = 0.00001);
3084        assert_relative_eq!(a3r3, std::f32::consts::PI, epsilon = 0.00001);
3085
3086        let a4r = vec4.angle_between(refvec);
3087        let a4r3 = Airships::angle_between_vectors_ccw(vec4.xy(), refvec.xy());
3088        assert_relative_eq!(a4r, std::f32::consts::FRAC_PI_2, epsilon = 0.00001);
3089        assert_relative_eq!(a4r3, std::f32::consts::FRAC_PI_2 * 3.0, epsilon = 0.00001);
3090    }
3091
3092    #[test]
3093    fn airship_angles_test() {
3094        let refvec = Vec2::new(0.0f32, 37.0);
3095        let ovec = Vec2::new(-4.0f32, -14.0);
3096        let oveccw0 = Vec2::new(-4, -14);
3097        let oveccw90 = Vec2::new(-14, 4);
3098        let oveccw180 = Vec2::new(4, 14);
3099        let oveccw270 = Vec2::new(14, -4);
3100        let ovecccw0 = Vec2::new(-4, -14);
3101        let ovecccw90 = Vec2::new(14, -4);
3102        let ovecccw180 = Vec2::new(4, 14);
3103        let ovecccw270 = Vec2::new(-14, 4);
3104
3105        let vec1 = Vec2::new(0.0f32, 37.0);
3106        let vec2 = Vec2::new(37.0f32, 0.0);
3107        let vec3 = Vec2::new(0.0f32, -37.0);
3108        let vec4 = Vec2::new(-37.0f32, 0.0);
3109
3110        assert!(
3111            ovec.rotated_z(Airships::angle_between_vectors_cw(vec1, refvec))
3112                .map(|x| x.round() as i32)
3113                == oveccw0
3114        );
3115        assert!(
3116            ovec.rotated_z(Airships::angle_between_vectors_cw(vec2, refvec))
3117                .map(|x| x.round() as i32)
3118                == oveccw90
3119        );
3120        assert!(
3121            ovec.rotated_z(Airships::angle_between_vectors_cw(vec3, refvec))
3122                .map(|x| x.round() as i32)
3123                == oveccw180
3124        );
3125        assert!(
3126            ovec.rotated_z(Airships::angle_between_vectors_cw(vec4, refvec))
3127                .map(|x| x.round() as i32)
3128                == oveccw270
3129        );
3130
3131        assert!(
3132            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec1, refvec))
3133                .map(|x| x.round() as i32)
3134                == ovecccw0
3135        );
3136        assert!(
3137            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec2, refvec))
3138                .map(|x| x.round() as i32)
3139                == ovecccw90
3140        );
3141        assert!(
3142            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec3, refvec))
3143                .map(|x| x.round() as i32)
3144                == ovecccw180
3145        );
3146        assert!(
3147            ovec.rotated_z(Airships::angle_between_vectors_ccw(vec4, refvec))
3148                .map(|x| x.round() as i32)
3149                == ovecccw270
3150        );
3151    }
3152
3153    #[test]
3154    fn airship_vec_test() {
3155        {
3156            let dock_pos = Vec3::new(10.0f32, 10.0, 0.0);
3157            let airship_dock_center = Vec2::new(0.0, 0.0);
3158            let mut left_tested = false;
3159            let mut right_tested = false;
3160            {
3161                for _ in 0..1000 {
3162                    let (airship_pos, airship_dir) =
3163                        Airships::airship_vec_for_docking_pos(dock_pos, airship_dock_center, None);
3164                    if airship_pos.x > 23.0 {
3165                        assert_relative_eq!(
3166                            airship_pos,
3167                            Vec3 {
3168                                x: 23.435028,
3169                                y: 22.020815,
3170                                z: -3.0
3171                            },
3172                            epsilon = 0.00001
3173                        );
3174                        assert_relative_eq!(
3175                            airship_dir.to_vec(),
3176                            Vec3 {
3177                                x: -0.70710677,
3178                                y: 0.70710677,
3179                                z: 0.0
3180                            },
3181                            epsilon = 0.00001
3182                        );
3183                        left_tested = true;
3184                    } else {
3185                        assert_relative_eq!(
3186                            airship_pos,
3187                            Vec3 {
3188                                x: 22.020815,
3189                                y: 23.435028,
3190                                z: -3.0
3191                            },
3192                            epsilon = 0.00001
3193                        );
3194                        assert_relative_eq!(
3195                            airship_dir.to_vec(),
3196                            Vec3 {
3197                                x: 0.70710677,
3198                                y: -0.70710677,
3199                                z: 0.0
3200                            },
3201                            epsilon = 0.00001
3202                        );
3203                        right_tested = true;
3204                    }
3205                    if left_tested && right_tested {
3206                        break;
3207                    }
3208                }
3209            }
3210            {
3211                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
3212                    dock_pos,
3213                    airship_dock_center,
3214                    Some(AirshipDockingSide::Port),
3215                );
3216                assert_relative_eq!(
3217                    airship_pos,
3218                    Vec3 {
3219                        x: 23.435028,
3220                        y: 22.020815,
3221                        z: -3.0
3222                    },
3223                    epsilon = 0.00001
3224                );
3225                assert_relative_eq!(
3226                    airship_dir.to_vec(),
3227                    Vec3 {
3228                        x: -0.70710677,
3229                        y: 0.70710677,
3230                        z: 0.0
3231                    },
3232                    epsilon = 0.00001
3233                );
3234            }
3235            {
3236                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
3237                    dock_pos,
3238                    airship_dock_center,
3239                    Some(AirshipDockingSide::Starboard),
3240                );
3241                assert_relative_eq!(
3242                    airship_pos,
3243                    Vec3 {
3244                        x: 22.020815,
3245                        y: 23.435028,
3246                        z: -3.0
3247                    },
3248                    epsilon = 0.00001
3249                );
3250                assert_relative_eq!(
3251                    airship_dir.to_vec(),
3252                    Vec3 {
3253                        x: 0.70710677,
3254                        y: -0.70710677,
3255                        z: 0.0
3256                    },
3257                    epsilon = 0.00001
3258                );
3259            }
3260        }
3261        {
3262            let dock_pos = Vec3::new(28874.0, 18561.0, 0.0);
3263            let airship_dock_center = Vec2::new(28911.0, 18561.0);
3264            {
3265                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
3266                    dock_pos,
3267                    airship_dock_center,
3268                    Some(AirshipDockingSide::Port),
3269                );
3270                assert_relative_eq!(
3271                    airship_pos,
3272                    Vec3 {
3273                        x: 28856.0,
3274                        y: 18562.0,
3275                        z: -3.0
3276                    },
3277                    epsilon = 0.00001
3278                );
3279                assert_relative_eq!(
3280                    airship_dir.to_vec(),
3281                    Vec3 {
3282                        x: 4.371139e-8,
3283                        y: -1.0,
3284                        z: 0.0
3285                    },
3286                    epsilon = 0.00001
3287                );
3288            }
3289            {
3290                let (airship_pos, airship_dir) = Airships::airship_vec_for_docking_pos(
3291                    dock_pos,
3292                    airship_dock_center,
3293                    Some(AirshipDockingSide::Starboard),
3294                );
3295                assert_relative_eq!(
3296                    airship_pos,
3297                    Vec3 {
3298                        x: 28856.0,
3299                        y: 18560.0,
3300                        z: -3.0
3301                    },
3302                    epsilon = 0.00001
3303                );
3304                assert_relative_eq!(
3305                    airship_dir.to_vec(),
3306                    Vec3 {
3307                        x: -1.1924881e-8,
3308                        y: 1.0,
3309                        z: 0.0
3310                    },
3311                    epsilon = 0.00001
3312                );
3313            }
3314        }
3315    }
3316
3317    #[test]
3318    fn angle_score_test() {
3319        let rt_angles = [
3320            0.0,
3321            std::f32::consts::FRAC_PI_2,
3322            std::f32::consts::PI,
3323            std::f32::consts::FRAC_PI_2 * 3.0,
3324        ];
3325        let con_angles = [
3326            0.0,
3327            std::f32::consts::FRAC_PI_2,
3328            std::f32::consts::PI,
3329            std::f32::consts::FRAC_PI_2 * 3.0,
3330        ];
3331        let scores = [
3332            [0.0, 2.5, 5.0, 2.5],
3333            [2.5, 0.0, 2.5, 5.0],
3334            [5.0, 2.5, 0.0, 2.5],
3335            [2.5, 5.0, 2.5, 0.0],
3336        ];
3337        let score_fn2 = |a1: f32, a2: f32| {
3338            let optimal_angle = (a1 + std::f32::consts::PI).rem_euclid(std::f32::consts::TAU);
3339            let angle_diff = (optimal_angle - a2)
3340                .abs()
3341                .min(std::f32::consts::TAU - (optimal_angle - a2).abs());
3342            (1.0 - (angle_diff / std::f32::consts::PI)) * 5.0
3343        };
3344        let mut i = 0;
3345        let mut j = 0;
3346        rt_angles.iter().for_each(|rt_angle| {
3347            j = 0;
3348            con_angles.iter().for_each(|con_angle| {
3349                let score = score_fn2(*con_angle, *rt_angle);
3350                assert_relative_eq!(score, scores[i][j], epsilon = 0.00001);
3351                j += 1;
3352            });
3353            i += 1;
3354        });
3355    }
3356
3357    #[test]
3358    fn best_eulerian_circuit_test() {
3359        let node_connections: DHashMap<usize, DockNode> = DHashMap::from_iter([
3360            (0, DockNode {
3361                node_id: 0,
3362                on_hull: false,
3363                connected: Vec::from_iter([23, 29, 26, 14, 19, 4]),
3364            }),
3365            (28, DockNode {
3366                node_id: 28,
3367                on_hull: false,
3368                connected: Vec::from_iter([23, 15, 25, 20, 21, 22]),
3369            }),
3370            (25, DockNode {
3371                node_id: 25,
3372                on_hull: false,
3373                connected: Vec::from_iter([23, 11, 28, 21]),
3374            }),
3375            (22, DockNode {
3376                node_id: 22,
3377                on_hull: false,
3378                connected: Vec::from_iter([23, 28, 27, 9, 3, 15]),
3379            }),
3380            (19, DockNode {
3381                node_id: 19,
3382                on_hull: false,
3383                connected: Vec::from_iter([0, 6, 29, 18, 2, 4]),
3384            }),
3385            (16, DockNode {
3386                node_id: 16,
3387                on_hull: false,
3388                connected: Vec::from_iter([10, 12, 20, 21]),
3389            }),
3390            (13, DockNode {
3391                node_id: 13,
3392                on_hull: true,
3393                connected: Vec::from_iter([7, 26, 9, 27, 3, 18]),
3394            }),
3395            (10, DockNode {
3396                node_id: 10,
3397                on_hull: false,
3398                connected: Vec::from_iter([24, 29, 11, 2, 16, 21]),
3399            }),
3400            (7, DockNode {
3401                node_id: 7,
3402                on_hull: true,
3403                connected: Vec::from_iter([26, 1, 13, 11]),
3404            }),
3405            (4, DockNode {
3406                node_id: 4,
3407                on_hull: false,
3408                connected: Vec::from_iter([0, 6, 14, 19]),
3409            }),
3410            (1, DockNode {
3411                node_id: 1,
3412                on_hull: true,
3413                connected: Vec::from_iter([7, 26, 8, 17]),
3414            }),
3415            (29, DockNode {
3416                node_id: 29,
3417                on_hull: false,
3418                connected: Vec::from_iter([0, 10, 24, 23, 19, 2]),
3419            }),
3420            (26, DockNode {
3421                node_id: 26,
3422                on_hull: false,
3423                connected: Vec::from_iter([0, 23, 14, 1, 27, 5, 7, 13]),
3424            }),
3425            (23, DockNode {
3426                node_id: 23,
3427                on_hull: false,
3428                connected: Vec::from_iter([0, 29, 25, 22, 28, 24, 11, 26]),
3429            }),
3430            (20, DockNode {
3431                node_id: 20,
3432                on_hull: true,
3433                connected: Vec::from_iter([18, 28, 12, 15, 16, 21]),
3434            }),
3435            (17, DockNode {
3436                node_id: 17,
3437                on_hull: false,
3438                connected: Vec::from_iter([5, 6, 8, 1]),
3439            }),
3440            (14, DockNode {
3441                node_id: 14,
3442                on_hull: false,
3443                connected: Vec::from_iter([0, 5, 26, 4]),
3444            }),
3445            (11, DockNode {
3446                node_id: 11,
3447                on_hull: false,
3448                connected: Vec::from_iter([10, 24, 23, 25, 21, 7]),
3449            }),
3450            (8, DockNode {
3451                node_id: 8,
3452                on_hull: true,
3453                connected: Vec::from_iter([18, 6, 1, 17]),
3454            }),
3455            (5, DockNode {
3456                node_id: 5,
3457                on_hull: false,
3458                connected: Vec::from_iter([6, 26, 14, 17]),
3459            }),
3460            (2, DockNode {
3461                node_id: 2,
3462                on_hull: false,
3463                connected: Vec::from_iter([10, 29, 12, 19]),
3464            }),
3465            (27, DockNode {
3466                node_id: 27,
3467                on_hull: false,
3468                connected: Vec::from_iter([26, 9, 13, 22]),
3469            }),
3470            (24, DockNode {
3471                node_id: 24,
3472                on_hull: false,
3473                connected: Vec::from_iter([10, 29, 11, 23]),
3474            }),
3475            (21, DockNode {
3476                node_id: 21,
3477                on_hull: false,
3478                connected: Vec::from_iter([10, 11, 25, 28, 20, 16]),
3479            }),
3480            (18, DockNode {
3481                node_id: 18,
3482                on_hull: true,
3483                connected: Vec::from_iter([6, 12, 8, 19, 20, 13]),
3484            }),
3485            (15, DockNode {
3486                node_id: 15,
3487                on_hull: true,
3488                connected: Vec::from_iter([28, 20, 3, 22]),
3489            }),
3490            (12, DockNode {
3491                node_id: 12,
3492                on_hull: false,
3493                connected: Vec::from_iter([18, 2, 16, 20]),
3494            }),
3495            (9, DockNode {
3496                node_id: 9,
3497                on_hull: false,
3498                connected: Vec::from_iter([13, 27, 3, 22]),
3499            }),
3500            (6, DockNode {
3501                node_id: 6,
3502                on_hull: false,
3503                connected: Vec::from_iter([4, 8, 5, 18, 19, 17]),
3504            }),
3505            (3, DockNode {
3506                node_id: 3,
3507                on_hull: true,
3508                connected: Vec::from_iter([13, 9, 15, 22]),
3509            }),
3510        ]);
3511
3512        let (best_segments, circuit, max_seg_len, min_spread, iteration) =
3513            find_best_eulerian_circuit(&node_connections)
3514                .expect("a circuit should have been found");
3515        assert_eq!(max_seg_len, 4);
3516        assert_relative_eq!(min_spread, 1.0606601, epsilon = 0.0000001);
3517        assert_eq!(iteration, 6);
3518        let expected_segments = vec![
3519            vec![26, 0, 23, 29, 0, 14, 5, 6, 4, 0, 19, 6, 8, 18, 6, 17, 5, 26],
3520            vec![
3521                26, 23, 25, 11, 10, 24, 29, 10, 2, 29, 19, 18, 12, 2, 19, 4, 14, 26,
3522            ],
3523            vec![
3524                26, 1, 8, 17, 1, 7, 11, 24, 23, 22, 28, 23, 11, 21, 10, 16, 12, 20, 18, 13, 26,
3525            ],
3526            vec![
3527                26, 27, 9, 13, 27, 22, 9, 3, 15, 28, 25, 21, 28, 20, 16, 21, 20, 15, 22, 3, 13, 7,
3528                26,
3529            ],
3530        ];
3531        assert_eq!(best_segments, expected_segments);
3532        let expected_circuit = vec![
3533            13, 7, 26, 0, 23, 29, 0, 14, 5, 6, 4, 0, 19, 6, 8, 18, 6, 17, 5, 26, 23, 25, 11, 10,
3534            24, 29, 10, 2, 29, 19, 18, 12, 2, 19, 4, 14, 26, 1, 8, 17, 1, 7, 11, 24, 23, 22, 28,
3535            23, 11, 21, 10, 16, 12, 20, 18, 13, 26, 27, 9, 13, 27, 22, 9, 3, 15, 28, 25, 21, 28,
3536            20, 16, 21, 20, 15, 22, 3, 13,
3537        ];
3538        assert_eq!(circuit, expected_circuit);
3539    }
3540
3541    fn large_map_docking_locations() -> Vec<Vec2<f32>> {
3542        [
3543            [384, 113],
3544            [713, 67],
3545            [1351, 17],
3546            [3146, 64],
3547            [720, 248],
3548            [775, 204],
3549            [829, 166],
3550            [1391, 161],
3551            [1812, 156],
3552            [3022, 204],
3553            [3094, 193],
3554            [781, 529],
3555            [860, 289],
3556            [889, 371],
3557            [892, 488],
3558            [975, 408],
3559            [1039, 509],
3560            [1050, 449],
3561            [1167, 379],
3562            [1359, 457],
3563            [1425, 382],
3564            [1468, 424],
3565            [1493, 363],
3566            [1752, 322],
3567            [1814, 452],
3568            [2139, 469],
3569            [2179, 343],
3570            [2283, 333],
3571            [2428, 299],
3572            [2499, 504],
3573            [2567, 498],
3574            [3110, 363],
3575            [3126, 503],
3576            [3248, 330],
3577            [3343, 491],
3578            [96, 837],
3579            [98, 752],
3580            [149, 884],
3581            [258, 679],
3582            [349, 873],
3583            [350, 676],
3584            [431, 983],
3585            [541, 842],
3586            [686, 640],
3587            [923, 728],
3588            [941, 537],
3589            [951, 654],
3590            [991, 575],
3591            [999, 955],
3592            [1164, 767],
3593            [1238, 669],
3594            [1250, 923],
3595            [1266, 808],
3596            [1343, 878],
3597            [1535, 711],
3598            [1633, 773],
3599            [1684, 705],
3600            [1690, 833],
3601            [1694, 982],
3602            [1742, 774],
3603            [1781, 821],
3604            [1833, 558],
3605            [1854, 623],
3606            [2169, 815],
3607            [2189, 966],
3608            [2232, 691],
3609            [2243, 778],
3610            [2266, 934],
3611            [2354, 742],
3612            [2423, 753],
3613            [2423, 999],
3614            [2438, 637],
3615            [2491, 758],
3616            [2497, 636],
3617            [2507, 855],
3618            [3066, 909],
3619            [3088, 568],
3620            [3124, 687],
3621            [3198, 681],
3622            [3241, 901],
3623            [3260, 603],
3624            [3276, 704],
3625            [3314, 652],
3626            [3329, 744],
3627            [3374, 888],
3628            [3513, 999],
3629            [3609, 708],
3630            [3864, 934],
3631            [3959, 933],
3632            [3959, 1000],
3633            [167, 1135],
3634            [229, 1072],
3635            [333, 1198],
3636            [349, 1481],
3637            [399, 1165],
3638            [473, 1350],
3639            [510, 1032],
3640            [523, 1481],
3641            [535, 1294],
3642            [552, 1080],
3643            [587, 1388],
3644            [789, 1103],
3645            [816, 1284],
3646            [886, 1183],
3647            [905, 1338],
3648            [1022, 1158],
3649            [1161, 1359],
3650            [1187, 1457],
3651            [1197, 1289],
3652            [1231, 1067],
3653            [1311, 1352],
3654            [1331, 1076],
3655            [1340, 1504],
3656            [1367, 1415],
3657            [1414, 1384],
3658            [1424, 1091],
3659            [1447, 1018],
3660            [1642, 1383],
3661            [1733, 1237],
3662            [1740, 1066],
3663            [1751, 1128],
3664            [1797, 1171],
3665            [1802, 1060],
3666            [1960, 1495],
3667            [1977, 1081],
3668            [2305, 1064],
3669            [2372, 1117],
3670            [2411, 1480],
3671            [2688, 1320],
3672            [2745, 1359],
3673            [2819, 1162],
3674            [2860, 1268],
3675            [2868, 1088],
3676            [2934, 1481],
3677            [2991, 1388],
3678            [3078, 1447],
3679            [3166, 1267],
3680            [3222, 1374],
3681            [3234, 1234],
3682            [3244, 1057],
3683            [3256, 1437],
3684            [3302, 1274],
3685            [3354, 1165],
3686            [3389, 1340],
3687            [3416, 1406],
3688            [3451, 1122],
3689            [3594, 1205],
3690            [3681, 1435],
3691            [3838, 1265],
3692            [3892, 1181],
3693            [3911, 1243],
3694            [200, 1663],
3695            [328, 1843],
3696            [363, 1630],
3697            [445, 1640],
3698            [505, 1756],
3699            [537, 1594],
3700            [560, 1779],
3701            [654, 1594],
3702            [713, 1559],
3703            [769, 1912],
3704            [970, 1782],
3705            [988, 1705],
3706            [1361, 1595],
3707            [1370, 1949],
3708            [1480, 1695],
3709            [1695, 1531],
3710            [1881, 1703],
3711            [2315, 1979],
3712            [2411, 1536],
3713            [2508, 1990],
3714            [2679, 1737],
3715            [2731, 1704],
3716            [2734, 1956],
3717            [2739, 1606],
3718            [2770, 1781],
3719            [2778, 1879],
3720            [2781, 1664],
3721            [2841, 1716],
3722            [2858, 1647],
3723            [2858, 1826],
3724            [2898, 1715],
3725            [2935, 1554],
3726            [3051, 1837],
3727            [3060, 1965],
3728            [3185, 1918],
3729            [3251, 1869],
3730            [3442, 1856],
3731            [3447, 1543],
3732            [3534, 1951],
3733            [3590, 1878],
3734            [3611, 1960],
3735            [3635, 1584],
3736            [3649, 1781],
3737            [3656, 1850],
3738            [3668, 1912],
3739            [3750, 1906],
3740            [3762, 1826],
3741            [3831, 1971],
3742            [3841, 1876],
3743            [3888, 1806],
3744            [3960, 1818],
3745            [177, 2260],
3746            [239, 2026],
3747            [358, 2364],
3748            [471, 2327],
3749            [528, 2100],
3750            [536, 2198],
3751            [588, 2244],
3752            [648, 2180],
3753            [665, 2038],
3754            [693, 2366],
3755            [852, 2410],
3756            [898, 2293],
3757            [969, 2205],
3758            [1095, 2322],
3759            [1198, 2217],
3760            [1267, 2284],
3761            [1278, 2220],
3762            [1339, 2114],
3763            [1419, 2203],
3764            [1470, 2049],
3765            [1487, 2108],
3766            [1959, 2257],
3767            [2087, 2061],
3768            [2226, 2048],
3769            [2231, 2319],
3770            [2385, 2251],
3771            [2417, 2039],
3772            [2598, 2035],
3773            [2686, 2071],
3774            [2715, 2204],
3775            [2778, 2188],
3776            [2900, 2128],
3777            [2910, 2007],
3778            [2988, 2087],
3779            [3002, 2435],
3780            [3082, 2433],
3781            [3115, 2006],
3782            [3167, 2143],
3783            [3170, 2361],
3784            [3360, 2433],
3785            [3472, 2370],
3786            [3514, 2022],
3787            [3599, 2045],
3788            [3662, 2365],
3789            [3676, 2172],
3790            [3838, 2208],
3791            [3921, 2060],
3792            [87, 2628],
3793            [239, 2604],
3794            [270, 2668],
3795            [327, 2726],
3796            [371, 2781],
3797            [419, 2583],
3798            [546, 2574],
3799            [620, 2776],
3800            [979, 2850],
3801            [1052, 2762],
3802            [1095, 2825],
3803            [1486, 2601],
3804            [1587, 2701],
3805            [1620, 2599],
3806            [1633, 2492],
3807            [1948, 2809],
3808            [2156, 2852],
3809            [2464, 2605],
3810            [2544, 2777],
3811            [2645, 2605],
3812            [2743, 2466],
3813            [2836, 2785],
3814            [2981, 2635],
3815            [3029, 2699],
3816            [3162, 2733],
3817            [3389, 2769],
3818            [3484, 2776],
3819            [3561, 2795],
3820            [3631, 2549],
3821            [3669, 2474],
3822            [3732, 2625],
3823            [33, 3129],
3824            [97, 3152],
3825            [191, 3289],
3826            [449, 2938],
3827            [450, 3000],
3828            [590, 3142],
3829            [654, 3065],
3830            [744, 3093],
3831            [870, 3042],
3832            [875, 2904],
3833            [921, 3103],
3834            [1018, 3034],
3835            [1040, 3135],
3836            [1079, 3238],
3837            [1122, 3316],
3838            [1136, 2996],
3839            [1237, 3366],
3840            [1294, 3127],
3841            [1360, 3297],
3842            [1366, 3043],
3843            [1368, 2985],
3844            [1381, 3128],
3845            [1464, 3089],
3846            [1514, 2965],
3847            [1529, 3046],
3848            [1901, 3052],
3849            [1954, 3272],
3850            [2117, 3121],
3851            [2182, 3381],
3852            [2225, 3212],
3853            [2241, 3142],
3854            [2250, 2949],
3855            [2340, 3333],
3856            [2395, 3195],
3857            [2496, 3383],
3858            [2521, 3162],
3859            [2604, 2959],
3860            [2635, 3287],
3861            [2644, 3021],
3862            [2657, 3140],
3863            [2716, 3367],
3864            [2726, 3184],
3865            [2734, 3264],
3866            [2799, 3300],
3867            [2866, 3361],
3868            [2907, 2893],
3869            [2938, 3362],
3870            [3058, 2982],
3871            [3187, 3076],
3872            [3357, 3200],
3873            [3467, 3300],
3874            [3511, 3359],
3875            [3522, 3105],
3876            [3538, 2997],
3877            [3791, 3348],
3878            [3866, 3261],
3879            [3947, 3223],
3880            [33, 3807],
3881            [109, 3828],
3882            [390, 3472],
3883            [468, 3510],
3884            [534, 3508],
3885            [563, 3659],
3886            [665, 3830],
3887            [668, 3732],
3888            [742, 3770],
3889            [896, 3818],
3890            [934, 3475],
3891            [1255, 3871],
3892            [1309, 3477],
3893            [1318, 3812],
3894            [1425, 3417],
3895            [1443, 3950],
3896            [1479, 3638],
3897            [1492, 3546],
3898            [1498, 3940],
3899            [1533, 3593],
3900            [1584, 3448],
3901            [1605, 3691],
3902            [1632, 3831],
3903            [1798, 3826],
3904            [1992, 3612],
3905            [2101, 3713],
3906            [2157, 3496],
3907            [2204, 3796],
3908            [2314, 3835],
3909            [2350, 3650],
3910            [2446, 3697],
3911            [2474, 3624],
3912            [2516, 3528],
3913            [2607, 3551],
3914            [2644, 3929],
3915            [2714, 3603],
3916            [2760, 3707],
3917            [2797, 3658],
3918            [2940, 3520],
3919            [2955, 3687],
3920            [2971, 3446],
3921            [3081, 3427],
3922            [3082, 3828],
3923            [3124, 3475],
3924            [3149, 3624],
3925            [3174, 3539],
3926            [3341, 3897],
3927            [3371, 3841],
3928            [3663, 3786],
3929            [3740, 3468],
3930            [3783, 3575],
3931            [3886, 3584],
3932            [3948, 3547],
3933        ]
3934        .iter()
3935        .map(|&[x, y]| Vec2::new(x as f32, y as f32))
3936        .collect()
3937    }
3938
3939    fn large_map_docking_points() -> Vec<Point> {
3940        large_map_docking_locations()
3941            .iter()
3942            .map(|&loc| Point {
3943                x: loc.x as f64,
3944                y: loc.y as f64,
3945            })
3946            .collect()
3947    }
3948
3949    #[test]
3950    fn large_map_graph_remove_edges_compare_test() {
3951        let all_dock_points1 = large_map_docking_points();
3952        let triangulation1 = triangulate(&all_dock_points1);
3953        let node_connections1 = triangulation1.node_connections();
3954
3955        let all_dock_points2 = large_map_docking_points();
3956        let triangulation2 = triangulate(&all_dock_points2);
3957        let node_connections2 = triangulation2.node_connections();
3958
3959        assert_eq!(
3960            all_dock_points1, all_dock_points2,
3961            "Dock points should be the same."
3962        );
3963        assert_eq!(
3964            node_connections1, node_connections2,
3965            "Node connections should be equal before removing edges."
3966        );
3967
3968        let max_distance_squared = 1000.0f64.powi(2);
3969
3970        let mut edges_to_remove1 = Vec::new();
3971        node_connections1.iter().for_each(|(node_id, node)| {
3972            for &connected_node_id in &node.connected {
3973                let pt1 = &all_dock_points1[*node_id];
3974                let pt2 = &all_dock_points1[connected_node_id];
3975                let v1 = Vec2 { x: pt1.x, y: pt1.y };
3976                let v2 = Vec2 { x: pt2.x, y: pt2.y };
3977                // Remove the edge if the distance is greater than 1000.0
3978                if v1.distance_squared(v2) > max_distance_squared {
3979                    edges_to_remove1.push((*node_id, connected_node_id));
3980                }
3981            }
3982        });
3983        let edges_to_remove1b = edges_to_remove1.clone();
3984
3985        let mut edges_to_remove1c = DHashSet::default();
3986        edges_to_remove1b
3987            .iter()
3988            .for_each(|&(node_id, connected_node_id)| {
3989                edges_to_remove1c.insert(if node_id < connected_node_id {
3990                    (node_id, connected_node_id)
3991                } else {
3992                    (connected_node_id, node_id)
3993                });
3994            });
3995
3996        let mut edges_to_remove2 = DHashSet::default();
3997        let all_edges2 = triangulation2.all_edges();
3998        for edge in all_edges2.iter() {
3999            let pt1 = &all_dock_points2[edge.0];
4000            let pt2 = &all_dock_points2[edge.1];
4001            let v1 = Vec2 { x: pt1.x, y: pt1.y };
4002            let v2 = Vec2 { x: pt2.x, y: pt2.y };
4003            // Remove the edge if the distance between the points is greater than
4004            // max_leg_length
4005            if v1.distance_squared(v2) > max_distance_squared {
4006                edges_to_remove2.insert(*edge);
4007            }
4008        }
4009
4010        assert_eq!(
4011            edges_to_remove1c, edges_to_remove2,
4012            "Edges to remove should be the same in hashset form."
4013        );
4014
4015        let mut mutable_node_connections1 = node_connections1.clone();
4016        for (node_id, connected_node_id) in edges_to_remove1 {
4017            remove_edge((node_id, connected_node_id), &mut mutable_node_connections1);
4018        }
4019
4020        let mut mutable_node_connections1b = node_connections1.clone();
4021        for edge in edges_to_remove1c {
4022            remove_edge(edge, &mut mutable_node_connections1b);
4023        }
4024
4025        assert_eq!(
4026            mutable_node_connections1, mutable_node_connections1b,
4027            "Node connections1 should be the same for either Vec or HashSet remove edges."
4028        );
4029
4030        let mut mutable_node_connections2 = node_connections2.clone();
4031        for edge in edges_to_remove2 {
4032            remove_edge(edge, &mut mutable_node_connections2);
4033        }
4034
4035        assert_eq!(
4036            mutable_node_connections1, mutable_node_connections2,
4037            "Node connections should be equal after removing edges."
4038        );
4039
4040        assert_eq!(
4041            mutable_node_connections1, mutable_node_connections2,
4042            "Node connections should be equal after removing edges."
4043        );
4044    }
4045
4046    #[test]
4047    fn docking_position_from_platform_test() {
4048        let airships = airships_from_test_data();
4049        let platforms = [
4050            AirshipDockPlatform::NorthPlatform,
4051            AirshipDockPlatform::EastPlatform,
4052            AirshipDockPlatform::SouthPlatform,
4053            AirshipDockPlatform::WestPlatform,
4054        ];
4055        let expected = [
4056            Vec3 {
4057                x: 26688.0,
4058                y: 4777.0,
4059                z: 213.0,
4060            },
4061            Vec3 {
4062                x: 26707.0,
4063                y: 4758.0,
4064                z: 213.0,
4065            },
4066            Vec3 {
4067                x: 26688.0,
4068                y: 4739.0,
4069                z: 213.0,
4070            },
4071            Vec3 {
4072                x: 26669.0,
4073                y: 4758.0,
4074                z: 213.0,
4075            },
4076            Vec3 {
4077                x: 24574.0,
4078                y: 26127.0,
4079                z: 214.0,
4080            },
4081            Vec3 {
4082                x: 24593.0,
4083                y: 26108.0,
4084                z: 214.0,
4085            },
4086            Vec3 {
4087                x: 24574.0,
4088                y: 26089.0,
4089                z: 214.0,
4090            },
4091            Vec3 {
4092                x: 24555.0,
4093                y: 26108.0,
4094                z: 214.0,
4095            },
4096            Vec3 {
4097                x: 24253.0,
4098                y: 20734.0,
4099                z: 515.0,
4100            },
4101            Vec3 {
4102                x: 24272.0,
4103                y: 20715.0,
4104                z: 515.0,
4105            },
4106            Vec3 {
4107                x: 24253.0,
4108                y: 20696.0,
4109                z: 515.0,
4110            },
4111            Vec3 {
4112                x: 24234.0,
4113                y: 20715.0,
4114                z: 515.0,
4115            },
4116            Vec3 {
4117                x: 20809.0,
4118                y: 6574.0,
4119                z: 216.0,
4120            },
4121            Vec3 {
4122                x: 20828.0,
4123                y: 6555.0,
4124                z: 216.0,
4125            },
4126            Vec3 {
4127                x: 20809.0,
4128                y: 6536.0,
4129                z: 216.0,
4130            },
4131            Vec3 {
4132                x: 20790.0,
4133                y: 6555.0,
4134                z: 216.0,
4135            },
4136            Vec3 {
4137                x: 16492.0,
4138                y: 1080.0,
4139                z: 211.0,
4140            },
4141            Vec3 {
4142                x: 16511.0,
4143                y: 1061.0,
4144                z: 211.0,
4145            },
4146            Vec3 {
4147                x: 16492.0,
4148                y: 1042.0,
4149                z: 211.0,
4150            },
4151            Vec3 {
4152                x: 16473.0,
4153                y: 1061.0,
4154                z: 211.0,
4155            },
4156            Vec3 {
4157                x: 18452.0,
4158                y: 11255.0,
4159                z: 421.0,
4160            },
4161            Vec3 {
4162                x: 18471.0,
4163                y: 11236.0,
4164                z: 421.0,
4165            },
4166            Vec3 {
4167                x: 18452.0,
4168                y: 11217.0,
4169                z: 421.0,
4170            },
4171            Vec3 {
4172                x: 18433.0,
4173                y: 11236.0,
4174                z: 421.0,
4175            },
4176            Vec3 {
4177                x: 21870.0,
4178                y: 8549.0,
4179                z: 216.0,
4180            },
4181            Vec3 {
4182                x: 21889.0,
4183                y: 8530.0,
4184                z: 216.0,
4185            },
4186            Vec3 {
4187                x: 21870.0,
4188                y: 8511.0,
4189                z: 216.0,
4190            },
4191            Vec3 {
4192                x: 21851.0,
4193                y: 8530.0,
4194                z: 216.0,
4195            },
4196            Vec3 {
4197                x: 22577.0,
4198                y: 15225.0,
4199                z: 277.0,
4200            },
4201            Vec3 {
4202                x: 22605.0,
4203                y: 15197.0,
4204                z: 277.0,
4205            },
4206            Vec3 {
4207                x: 22577.0,
4208                y: 15169.0,
4209                z: 277.0,
4210            },
4211            Vec3 {
4212                x: 22549.0,
4213                y: 15197.0,
4214                z: 277.0,
4215            },
4216            Vec3 {
4217                x: 5477.0,
4218                y: 15244.0,
4219                z: 1675.0,
4220            },
4221            Vec3 {
4222                x: 5514.0,
4223                y: 15207.0,
4224                z: 1675.0,
4225            },
4226            Vec3 {
4227                x: 5477.0,
4228                y: 15170.0,
4229                z: 1675.0,
4230            },
4231            Vec3 {
4232                x: 5440.0,
4233                y: 15207.0,
4234                z: 1675.0,
4235            },
4236            Vec3 {
4237                x: 23884.0,
4238                y: 24321.0,
4239                z: 214.0,
4240            },
4241            Vec3 {
4242                x: 23903.0,
4243                y: 24302.0,
4244                z: 214.0,
4245            },
4246            Vec3 {
4247                x: 23884.0,
4248                y: 24283.0,
4249                z: 214.0,
4250            },
4251            Vec3 {
4252                x: 23865.0,
4253                y: 24302.0,
4254                z: 214.0,
4255            },
4256            Vec3 {
4257                x: 13373.0,
4258                y: 2332.0,
4259                z: 259.0,
4260            },
4261            Vec3 {
4262                x: 13392.0,
4263                y: 2313.0,
4264                z: 259.0,
4265            },
4266            Vec3 {
4267                x: 13373.0,
4268                y: 2294.0,
4269                z: 259.0,
4270            },
4271            Vec3 {
4272                x: 13354.0,
4273                y: 2313.0,
4274                z: 259.0,
4275            },
4276            Vec3 {
4277                x: 20141.0,
4278                y: 31880.0,
4279                z: 215.0,
4280            },
4281            Vec3 {
4282                x: 20160.0,
4283                y: 31861.0,
4284                z: 215.0,
4285            },
4286            Vec3 {
4287                x: 20141.0,
4288                y: 31842.0,
4289                z: 215.0,
4290            },
4291            Vec3 {
4292                x: 20122.0,
4293                y: 31861.0,
4294                z: 215.0,
4295            },
4296            Vec3 {
4297                x: 29713.0,
4298                y: 24552.0,
4299                z: 214.0,
4300            },
4301            Vec3 {
4302                x: 29732.0,
4303                y: 24533.0,
4304                z: 214.0,
4305            },
4306            Vec3 {
4307                x: 29713.0,
4308                y: 24514.0,
4309                z: 214.0,
4310            },
4311            Vec3 {
4312                x: 29694.0,
4313                y: 24533.0,
4314                z: 214.0,
4315            },
4316            Vec3 {
4317                x: 18992.0,
4318                y: 17139.0,
4319                z: 435.0,
4320            },
4321            Vec3 {
4322                x: 19011.0,
4323                y: 17120.0,
4324                z: 435.0,
4325            },
4326            Vec3 {
4327                x: 18992.0,
4328                y: 17101.0,
4329                z: 435.0,
4330            },
4331            Vec3 {
4332                x: 18973.0,
4333                y: 17120.0,
4334                z: 435.0,
4335            },
4336            Vec3 {
4337                x: 7705.0,
4338                y: 12570.0,
4339                z: 1911.0,
4340            },
4341            Vec3 {
4342                x: 7742.0,
4343                y: 12533.0,
4344                z: 1911.0,
4345            },
4346            Vec3 {
4347                x: 7705.0,
4348                y: 12496.0,
4349                z: 1911.0,
4350            },
4351            Vec3 {
4352                x: 7668.0,
4353                y: 12533.0,
4354                z: 1911.0,
4355            },
4356            Vec3 {
4357                x: 30365.0,
4358                y: 13015.0,
4359                z: 244.0,
4360            },
4361            Vec3 {
4362                x: 30393.0,
4363                y: 12987.0,
4364                z: 244.0,
4365            },
4366            Vec3 {
4367                x: 30365.0,
4368                y: 12959.0,
4369                z: 244.0,
4370            },
4371            Vec3 {
4372                x: 30337.0,
4373                y: 12987.0,
4374                z: 244.0,
4375            },
4376            Vec3 {
4377                x: 10142.0,
4378                y: 19218.0,
4379                z: 1141.0,
4380            },
4381            Vec3 {
4382                x: 10170.0,
4383                y: 19190.0,
4384                z: 1141.0,
4385            },
4386            Vec3 {
4387                x: 10142.0,
4388                y: 19162.0,
4389                z: 1141.0,
4390            },
4391            Vec3 {
4392                x: 10114.0,
4393                y: 19190.0,
4394                z: 1141.0,
4395            },
4396            Vec3 {
4397                x: 13716.0,
4398                y: 17542.0,
4399                z: 1420.0,
4400            },
4401            Vec3 {
4402                x: 13753.0,
4403                y: 17505.0,
4404                z: 1420.0,
4405            },
4406            Vec3 {
4407                x: 13716.0,
4408                y: 17468.0,
4409                z: 1420.0,
4410            },
4411            Vec3 {
4412                x: 13679.0,
4413                y: 17505.0,
4414                z: 1420.0,
4415            },
4416            Vec3 {
4417                x: 9383.0,
4418                y: 17173.0,
4419                z: 909.0,
4420            },
4421            Vec3 {
4422                x: 9411.0,
4423                y: 17145.0,
4424                z: 909.0,
4425            },
4426            Vec3 {
4427                x: 9383.0,
4428                y: 17117.0,
4429                z: 909.0,
4430            },
4431            Vec3 {
4432                x: 9355.0,
4433                y: 17145.0,
4434                z: 909.0,
4435            },
4436            Vec3 {
4437                x: 24424.0,
4438                y: 7819.0,
4439                z: 329.0,
4440            },
4441            Vec3 {
4442                x: 24443.0,
4443                y: 7800.0,
4444                z: 329.0,
4445            },
4446            Vec3 {
4447                x: 24424.0,
4448                y: 7781.0,
4449                z: 329.0,
4450            },
4451            Vec3 {
4452                x: 24405.0,
4453                y: 7800.0,
4454                z: 329.0,
4455            },
4456            Vec3 {
4457                x: 7528.0,
4458                y: 28445.0,
4459                z: 218.0,
4460            },
4461            Vec3 {
4462                x: 7547.0,
4463                y: 28426.0,
4464                z: 218.0,
4465            },
4466            Vec3 {
4467                x: 7528.0,
4468                y: 28407.0,
4469                z: 218.0,
4470            },
4471            Vec3 {
4472                x: 7509.0,
4473                y: 28426.0,
4474                z: 218.0,
4475            },
4476            Vec3 {
4477                x: 9942.0,
4478                y: 30955.0,
4479                z: 185.0,
4480            },
4481            Vec3 {
4482                x: 9961.0,
4483                y: 30936.0,
4484                z: 185.0,
4485            },
4486            Vec3 {
4487                x: 9942.0,
4488                y: 30917.0,
4489                z: 185.0,
4490            },
4491            Vec3 {
4492                x: 9923.0,
4493                y: 30936.0,
4494                z: 185.0,
4495            },
4496            Vec3 {
4497                x: 27915.0,
4498                y: 18578.0,
4499                z: 498.0,
4500            },
4501            Vec3 {
4502                x: 27934.0,
4503                y: 18559.0,
4504                z: 498.0,
4505            },
4506            Vec3 {
4507                x: 27915.0,
4508                y: 18540.0,
4509                z: 498.0,
4510            },
4511            Vec3 {
4512                x: 27896.0,
4513                y: 18559.0,
4514                z: 498.0,
4515            },
4516            Vec3 {
4517                x: 3688.0,
4518                y: 29191.0,
4519                z: 198.0,
4520            },
4521            Vec3 {
4522                x: 3711.0,
4523                y: 29168.0,
4524                z: 198.0,
4525            },
4526            Vec3 {
4527                x: 3688.0,
4528                y: 29145.0,
4529                z: 198.0,
4530            },
4531            Vec3 {
4532                x: 3665.0,
4533                y: 29168.0,
4534                z: 198.0,
4535            },
4536            Vec3 {
4537                x: 15864.0,
4538                y: 15612.0,
4539                z: 419.0,
4540            },
4541            Vec3 {
4542                x: 15892.0,
4543                y: 15584.0,
4544                z: 419.0,
4545            },
4546            Vec3 {
4547                x: 15864.0,
4548                y: 15556.0,
4549                z: 419.0,
4550            },
4551            Vec3 {
4552                x: 15836.0,
4553                y: 15584.0,
4554                z: 419.0,
4555            },
4556            Vec3 {
4557                x: 9975.0,
4558                y: 24326.0,
4559                z: 755.0,
4560            },
4561            Vec3 {
4562                x: 10012.0,
4563                y: 24289.0,
4564                z: 755.0,
4565            },
4566            Vec3 {
4567                x: 9975.0,
4568                y: 24252.0,
4569                z: 755.0,
4570            },
4571            Vec3 {
4572                x: 9938.0,
4573                y: 24289.0,
4574                z: 755.0,
4575            },
4576            Vec3 {
4577                x: 479.0,
4578                y: 18316.0,
4579                z: 449.0,
4580            },
4581            Vec3 {
4582                x: 516.0,
4583                y: 18279.0,
4584                z: 449.0,
4585            },
4586            Vec3 {
4587                x: 479.0,
4588                y: 18242.0,
4589                z: 449.0,
4590            },
4591            Vec3 {
4592                x: 442.0,
4593                y: 18279.0,
4594                z: 449.0,
4595            },
4596            Vec3 {
4597                x: 26543.0,
4598                y: 17198.0,
4599                z: 362.0,
4600            },
4601            Vec3 {
4602                x: 26566.0,
4603                y: 17175.0,
4604                z: 362.0,
4605            },
4606            Vec3 {
4607                x: 26543.0,
4608                y: 17152.0,
4609                z: 362.0,
4610            },
4611            Vec3 {
4612                x: 26520.0,
4613                y: 17175.0,
4614                z: 362.0,
4615            },
4616        ];
4617
4618        for (i, dock_pos) in airships.airship_docks.iter().enumerate() {
4619            for platform in platforms {
4620                let docking_position = dock_pos.docking_position(platform);
4621                assert_eq!(docking_position, expected[i * 4 + platform as usize]);
4622            }
4623        }
4624    }
4625
4626    #[test]
4627    fn docking_transition_point_test() {
4628        let expected = [
4629            Vec3 {
4630                x: 26567.24,
4631                y: 4903.6567,
4632                z: 613.0,
4633            },
4634            Vec3 {
4635                x: 26725.146,
4636                y: 4948.012,
4637                z: 613.0,
4638            },
4639            Vec3 {
4640                x: 26825.607,
4641                y: 4668.8833,
4642                z: 613.0,
4643            },
4644            Vec3 {
4645                x: 26515.738,
4646                y: 4746.166,
4647                z: 613.0,
4648            },
4649            Vec3 {
4650                x: 26586.238,
4651                y: 4884.6543,
4652                z: 613.0,
4653            },
4654            Vec3 {
4655                x: 26744.012,
4656                y: 4929.0415,
4657                z: 613.0,
4658            },
4659            Vec3 {
4660                x: 26844.652,
4661                y: 4649.9404,
4662                z: 613.0,
4663            },
4664            Vec3 {
4665                x: 26534.713,
4666                y: 4727.306,
4667                z: 613.0,
4668            },
4669            Vec3 {
4670                x: 26567.326,
4671                y: 4865.7383,
4672                z: 613.0,
4673            },
4674            Vec3 {
4675                x: 26725.098,
4676                y: 4910.0225,
4677                z: 613.0,
4678            },
4679            Vec3 {
4680                x: 26826.025,
4681                y: 4631.4175,
4682                z: 613.0,
4683            },
4684            Vec3 {
4685                x: 26515.695,
4686                y: 4708.404,
4687                z: 613.0,
4688            },
4689            Vec3 {
4690                x: 26548.328,
4691                y: 4884.74,
4692                z: 613.0,
4693            },
4694            Vec3 {
4695                x: 26706.232,
4696                y: 4928.993,
4697                z: 613.0,
4698            },
4699            Vec3 {
4700                x: 26806.979,
4701                y: 4650.3584,
4702                z: 613.0,
4703            },
4704            Vec3 {
4705                x: 26496.72,
4706                y: 4727.2637,
4707                z: 613.0,
4708            },
4709            Vec3 {
4710                x: 21752.715,
4711                y: 8678.882,
4712                z: 766.0,
4713            },
4714            Vec3 {
4715                x: 21941.81,
4716                y: 8708.588,
4717                z: 766.0,
4718            },
4719            Vec3 {
4720                x: 22007.69,
4721                y: 8440.988,
4722                z: 766.0,
4723            },
4724            Vec3 {
4725                x: 21707.01,
4726                y: 8485.287,
4727                z: 766.0,
4728            },
4729            Vec3 {
4730                x: 21771.709,
4731                y: 8659.877,
4732                z: 766.0,
4733            },
4734            Vec3 {
4735                x: 21960.66,
4736                y: 8689.655,
4737                z: 766.0,
4738            },
4739            Vec3 {
4740                x: 22026.715,
4741                y: 8422.0205,
4742                z: 766.0,
4743            },
4744            Vec3 {
4745                x: 21725.943,
4746                y: 8466.458,
4747                z: 766.0,
4748            },
4749            Vec3 {
4750                x: 21752.816,
4751                y: 8640.974,
4752                z: 766.0,
4753            },
4754            Vec3 {
4755                x: 21941.717,
4756                y: 8670.63,
4757                z: 766.0,
4758            },
4759            Vec3 {
4760                x: 22007.924,
4761                y: 8403.286,
4762                z: 766.0,
4763            },
4764            Vec3 {
4765                x: 21706.914,
4766                y: 8447.533,
4767                z: 766.0,
4768            },
4769            Vec3 {
4770                x: 21733.822,
4771                y: 8659.979,
4772                z: 766.0,
4773            },
4774            Vec3 {
4775                x: 21922.867,
4776                y: 8689.562,
4777                z: 766.0,
4778            },
4779            Vec3 {
4780                x: 21988.898,
4781                y: 8422.254,
4782                z: 766.0,
4783            },
4784            Vec3 {
4785                x: 21687.98,
4786                y: 8466.362,
4787                z: 766.0,
4788            },
4789            Vec3 {
4790                x: 29544.33,
4791                y: 24598.639,
4792                z: 614.0,
4793            },
4794            Vec3 {
4795                x: 29773.992,
4796                y: 24716.027,
4797                z: 614.0,
4798            },
4799            Vec3 {
4800                x: 29734.61,
4801                y: 24378.34,
4802                z: 614.0,
4803            },
4804            Vec3 {
4805                x: 29578.096,
4806                y: 24440.527,
4807                z: 614.0,
4808            },
4809            Vec3 {
4810                x: 29563.35,
4811                y: 24579.71,
4812                z: 614.0,
4813            },
4814            Vec3 {
4815                x: 29792.535,
4816                y: 24697.197,
4817                z: 614.0,
4818            },
4819            Vec3 {
4820                x: 29753.492,
4821                y: 24359.324,
4822                z: 614.0,
4823            },
4824            Vec3 {
4825                x: 29597.02,
4826                y: 24421.621,
4827                z: 614.0,
4828            },
4829            Vec3 {
4830                x: 29544.385,
4831                y: 24560.84,
4832                z: 614.0,
4833            },
4834            Vec3 {
4835                x: 29773.744,
4836                y: 24678.12,
4837                z: 614.0,
4838            },
4839            Vec3 {
4840                x: 29734.64,
4841                y: 24340.344,
4842                z: 614.0,
4843            },
4844            Vec3 {
4845                x: 29578.012,
4846                y: 24402.63,
4847                z: 614.0,
4848            },
4849            Vec3 {
4850                x: 29525.365,
4851                y: 24579.768,
4852                z: 614.0,
4853            },
4854            Vec3 {
4855                x: 29755.2,
4856                y: 24696.95,
4857                z: 614.0,
4858            },
4859            Vec3 {
4860                x: 29715.758,
4861                y: 24359.357,
4862                z: 614.0,
4863            },
4864            Vec3 {
4865                x: 29559.088,
4866                y: 24421.537,
4867                z: 614.0,
4868            },
4869            Vec3 {
4870                x: 9292.779,
4871                y: 17322.951,
4872                z: 1459.0,
4873            },
4874            Vec3 {
4875                x: 9528.595,
4876                y: 17270.094,
4877                z: 1459.0,
4878            },
4879            Vec3 {
4880                x: 9524.052,
4881                y: 17069.418,
4882                z: 1459.0,
4883            },
4884            Vec3 {
4885                x: 9299.091,
4886                y: 17019.428,
4887                z: 1459.0,
4888            },
4889            Vec3 {
4890                x: 9320.701,
4891                y: 17294.904,
4892                z: 1459.0,
4893            },
4894            Vec3 {
4895                x: 9556.46,
4896                y: 17242.295,
4897                z: 1459.0,
4898            },
4899            Vec3 {
4900                x: 9552.073,
4901                y: 17041.447,
4902                z: 1459.0,
4903            },
4904            Vec3 {
4905                x: 9326.793,
4906                y: 16991.592,
4907                z: 1459.0,
4908            },
4909            Vec3 {
4910                x: 9293.017,
4911                y: 17267.094,
4912                z: 1459.0,
4913            },
4914            Vec3 {
4915                x: 9528.434,
4916                y: 17214.336,
4917                z: 1459.0,
4918            },
4919            Vec3 {
4920                x: 9524.213,
4921                y: 17013.637,
4922                z: 1459.0,
4923            },
4924            Vec3 {
4925                x: 9298.88,
4926                y: 16963.543,
4927                z: 1459.0,
4928            },
4929            Vec3 {
4930                x: 9265.096,
4931                y: 17295.14,
4932                z: 1459.0,
4933            },
4934            Vec3 {
4935                x: 9500.567,
4936                y: 17242.135,
4937                z: 1459.0,
4938            },
4939            Vec3 {
4940                x: 9496.191,
4941                y: 17041.607,
4942                z: 1459.0,
4943            },
4944            Vec3 {
4945                x: 9271.179,
4946                y: 16991.38,
4947                z: 1459.0,
4948            },
4949            Vec3 {
4950                x: 15745.189,
4951                y: 15740.487,
4952                z: 819.0,
4953            },
4954            Vec3 {
4955                x: 15986.825,
4956                y: 15736.656,
4957                z: 819.0,
4958            },
4959            Vec3 {
4960                x: 15992.56,
4961                y: 15493.267,
4962                z: 819.0,
4963            },
4964            Vec3 {
4965                x: 15739.27,
4966                y: 15489.251,
4967                z: 819.0,
4968            },
4969            Vec3 {
4970                x: 15773.181,
4971                y: 15712.4795,
4972                z: 819.0,
4973            },
4974            Vec3 {
4975                x: 16014.62,
4976                y: 15708.857,
4977                z: 819.0,
4978            },
4979            Vec3 {
4980                x: 16020.567,
4981                y: 15465.275,
4982                z: 819.0,
4983            },
4984            Vec3 {
4985                x: 15767.052,
4986                y: 15461.473,
4987                z: 819.0,
4988            },
4989            Vec3 {
4990                x: 15745.397,
4991                y: 15684.68,
4992                z: 819.0,
4993            },
4994            Vec3 {
4995                x: 15986.621,
4996                y: 15680.855,
4997                z: 819.0,
4998            },
4999            Vec3 {
5000                x: 15992.771,
5001                y: 15437.497,
5002                z: 819.0,
5003            },
5004            Vec3 {
5005                x: 15739.049,
5006                y: 15433.476,
5007                z: 819.0,
5008            },
5009            Vec3 {
5010                x: 15717.407,
5011                y: 15712.688,
5012                z: 819.0,
5013            },
5014            Vec3 {
5015                x: 15958.826,
5016                y: 15708.654,
5017                z: 819.0,
5018            },
5019            Vec3 {
5020                x: 15964.763,
5021                y: 15465.488,
5022                z: 819.0,
5023            },
5024            Vec3 {
5025                x: 15711.268,
5026                y: 15461.253,
5027                z: 819.0,
5028            },
5029        ];
5030
5031        let airships = airships_from_test_data();
5032        let platforms = [
5033            AirshipDockPlatform::NorthPlatform,
5034            AirshipDockPlatform::EastPlatform,
5035            AirshipDockPlatform::SouthPlatform,
5036            AirshipDockPlatform::WestPlatform,
5037        ];
5038        let from_positions = [
5039            Vec2::new(0.0, 32768.0),
5040            Vec2::new(32768.0, 32768.0),
5041            Vec2::new(32768.0, 0.0),
5042            Vec2::new(0.0, 0.0),
5043        ];
5044        for dock_index in (0..airships.airship_docks.len()).step_by(6) {
5045            for platform in platforms.iter() {
5046                for (i, from_pos) in from_positions.iter().enumerate() {
5047                    let transition_point = airships
5048                        .approach_transition_point(dock_index, dock_index % 4, *platform, *from_pos)
5049                        .unwrap();
5050                    assert_eq!(
5051                        transition_point,
5052                        expected[dock_index / 6 * 16 + *platform as usize * 4 + i]
5053                    );
5054                }
5055            }
5056        }
5057    }
5058
5059    #[test]
5060    fn docking_side_for_platform_test() {
5061        // Approximately: 0, 22, 45, 67, 90, 112, 135, 157, 180, 202, 225, 247, 270,
5062        // 292, 315, 337 degrees
5063        let dirs = [
5064            Vec2::new(0.0, 100.0) - Vec2::zero(),
5065            Vec2::new(100.0, 100.0) - Vec2::zero(),
5066            Vec2::new(100.0, 0.0) - Vec2::zero(),
5067            Vec2::new(100.0, -100.0) - Vec2::zero(),
5068            Vec2::new(0.0, -100.0) - Vec2::zero(),
5069            Vec2::new(-100.0, -100.0) - Vec2::zero(),
5070            Vec2::new(-100.0, 0.0) - Vec2::zero(),
5071            Vec2::new(-100.0, 100.0) - Vec2::zero(),
5072        ];
5073        let expected = [
5074            AirshipDockingSide::Port,
5075            AirshipDockingSide::Starboard,
5076            AirshipDockingSide::Starboard,
5077            AirshipDockingSide::Starboard,
5078            AirshipDockingSide::Starboard,
5079            AirshipDockingSide::Port,
5080            AirshipDockingSide::Port,
5081            AirshipDockingSide::Port,
5082            AirshipDockingSide::Port,
5083            AirshipDockingSide::Port,
5084            AirshipDockingSide::Port,
5085            AirshipDockingSide::Starboard,
5086            AirshipDockingSide::Starboard,
5087            AirshipDockingSide::Starboard,
5088            AirshipDockingSide::Starboard,
5089            AirshipDockingSide::Port,
5090            AirshipDockingSide::Starboard,
5091            AirshipDockingSide::Port,
5092            AirshipDockingSide::Port,
5093            AirshipDockingSide::Port,
5094            AirshipDockingSide::Port,
5095            AirshipDockingSide::Starboard,
5096            AirshipDockingSide::Starboard,
5097            AirshipDockingSide::Starboard,
5098            AirshipDockingSide::Starboard,
5099            AirshipDockingSide::Starboard,
5100            AirshipDockingSide::Starboard,
5101            AirshipDockingSide::Port,
5102            AirshipDockingSide::Port,
5103            AirshipDockingSide::Port,
5104            AirshipDockingSide::Port,
5105            AirshipDockingSide::Starboard,
5106        ];
5107        for platform in [
5108            AirshipDockPlatform::NorthPlatform,
5109            AirshipDockPlatform::EastPlatform,
5110            AirshipDockPlatform::SouthPlatform,
5111            AirshipDockPlatform::WestPlatform,
5112        ]
5113        .iter()
5114        {
5115            for (i, dir) in dirs.iter().enumerate() {
5116                let side = AirshipDockingSide::from_dir_to_platform(dir, platform);
5117                assert_eq!(side, expected[*platform as usize * 8 + i]);
5118            }
5119        }
5120    }
5121
5122    #[test]
5123    fn airship_spawning_locations_test() {
5124        let mut airships = airships_from_test_data();
5125        let all_dock_points = airships
5126            .airship_docks
5127            .iter()
5128            .map(|dock| Point {
5129                x: dock.center.x as f64,
5130                y: dock.center.y as f64,
5131            })
5132            .collect::<Vec<_>>();
5133
5134        airships.calculate_spawning_locations(&all_dock_points);
5135    }
5136}