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