Перейти к содержимому
k15

k15. Математика дрона: геокоординаты — lat/lon/alt, расстояние haversine, направление bearing

k15. Математика дрона: геокоординаты — lat/lon/alt, расстояние haversine, направление bearing

UAV Python Pro · Конспект K15 · Модуль M2. Математика дрона: геокоординаты — lat/lon/alt, расстояние haversine, направление bearing · Редакция 1.0 · После CHECKPOINT-K14

Модуль M2 выходит на финиш: на уроке K13 вы освоили векторы и системы координат, на уроке K14 — ориентацию аппарата. Последний элемент математического минимума — сама Земля. Миссии записываются таблицей точек с широтой и долготой, телеметрия возвращает позицию в тех же координатах, а границы безопасности задаются радиусом вокруг домашней точки. На этом уроке вы научитесь вычислять расстояние и направление между двумя географическими точками, смещать точку на заданное число метров в заданном направлении и строить простейшую геозону — всё каноническими формулами, покрытыми автоматическими тестами.

Географические координаты — это углы, а не метры: широта и долгота измеряются в градусах, причём один градус долготы соответствует разным расстояниям на разных широтах. Поэтому применять теорему Пифагора прямо к градусам нельзя — расстояние и направление на сфере вычисляются особыми формулами: гаверсинус для расстояния, формула начального азимута для направления, формула конечной точки для смещения. Мы берём эти формулы из классического справочника, реализуем их чистыми функциями и проверяем тестами — ровно тем способом, который вы отработали на двух предыдущих уроках.

Содержание

1. Цели урока

После изучения этого урока вы будете уметь выполнять следующие действия.

  • Вы объясните, что измеряют широта, долгота и высота, и почему координаты — это углы, а не метры.
  • Вы вычислите расстояние между двумя географическими точками по формуле гаверсинуса через функцию haversine_distance_m().
  • Вы вычислите направление (bearing) из одной точки на другую через функцию bearing_deg() и подадите результат в heading_to_velocity_ned() из урока K13.
  • Вы получите координаты точки, смещённой на заданное расстояние в заданном направлении, через функцию destination_point().
  • Вы реализуете простейшую геозону: проверка расстояния от домашней точки и потолка высоты через функцию geofence_status().
  • Вы напишете тесты геодезических функций трёх классов: опорные значения, свойства и согласованность цепочки «смещение → расстояние → направление».
  • Вы завершите модуль M2 «Математика дрона» и подготовитесь к модулю M3 «Протоколы и интерфейсы».
Связь с разработкой программного обеспечения для БПЛА. Сообщение MAVLink GLOBAL_POSITION_INT приносит широту, долготу и высоту аппарата; файл миссии представляет собой таблицу точек в тех же координатах; логика сопровождения цели в модуле M11 будет непрерывно вычислять расстояние и направление до объекта. Три функции этого урока — рабочий инструмент на все последующие модули курса.
↑ К оглавлению

2. Словарь урока

ТерминПростыми словами
Широта (latitude, lat) Угол к северу или югу от экватора: от −90° до +90°.
Долгота (longitude, lon) Угол к востоку или западу от Гринвичского меридиана: от −180° до +180°.
WGS-84 Всемирная геодезическая система 1984 года: модель формы Земли (эллипсоид) и система координат, в которой работают спутниковые приёмники GPS.
Высота над уровнем моря (MSL) Высота, отсчитанная от среднего уровня моря.
Относительная высота Высота над домашней точкой старта; в MAVLink — отдельное поле.
Большой круг Окружность на сфере с центром в центре сферы: экватор и любой меридиан. Кратчайший путь между точками идёт по его дуге.
Гаверсинус (haversine) Формула расстояния между двумя точками сферы по их широтам и долготам; численно устойчива на малых дистанциях.
Bearing (азимут, направление) Угол от направления на север по часовой стрелке до направления на целевую точку; тот же компасный договор, что у курса heading.
Destination point Конечная точка: координаты места, до которого нужно пройти заданное расстояние в заданном направлении.
Домашняя позиция (home) Точка старта, которую автопилот запоминает и к которой возвращается по команде RTL (Return To Launch).
Геозона (geofence) Виртуальная граница разрешённой зоны полёта; при выходе за границу система предпринимает защитное действие.
GLOBAL_POSITION_INT Сообщение MAVLink с глобальной позицией: координаты в целых числах, умноженных на 10⁷, высоты в миллиметрах.
↑ К оглавлению

3. Зачем программисту БПЛА геодезия

Векторы и ориентация из уроков K13–K14 живут в локальном мире: метры и секунды вокруг точки старта. Миссии и навигация живут в глобальном мире: широта, долгота и высота. Связка этих двух миров — постоянная работа программиста беспилотных систем. Вот четыре типичные задачи, которые решаются инструментами этого урока.

  • Построение маршрута: миссия «квадрат со стороной 200 метров вокруг точки старта» требует вычислить координаты четырёх углов — это смещение точки на расстояние в заданном направлении.
  • Чтение телеметрии: аппарат прислал позицию; чтобы понять, как далеко он от дома и в какую сторону движется, нужны расстояние и направление между двумя точками.
  • Безопасность: простейшая геозона обязана на каждом витке цикла управления сравнивать расстояние от дома и высоту с разрешёнными пределами.
  • Сопровождение цели: камера видит объект, его координаты оценены — чтобы навестись или следовать за ним, вычисляется направление и расстояние до цели (модуль M11).

Этот урок завершает модуль M2. Три модуля курса — M2 (математика), M3 (протоколы) и M5 (MAVLink) — образуют ядро профессии: математика говорит, что означают числа, протоколы — как числа передаются, MAVLink — какие именно числа летают между аппаратами и наземными станциями.

↑ К оглавлению

4. Широта, долгота, высота: WGS-84 интуитивно

Широта (latitude, обозначается греческой буквой φ, «фи») — это угол между плоскостью экватора и направлением из центра Земли на точку. Широта равна +90° на Северном полюсе, 0° на экваторе и −90° на Южном полюсе. Долгота (longitude, обозначается λ, «лямбда») — угол между плоскостью Гринвичского меридиана и плоскостью меридиана точки: от −180° на запад до +180° на восток. Третья координата — высота в метрах.

Широта (lat) — угол от плоскости экватора:
    +90 на Северном полюсе, 0 на экваторе, -90 на Южном.

Долгота (lon) — угол от Гринвичского меридиана:
    0 в Гринвиче, до +180 на восток, до -180 на запад.

Меридианы сходятся у полюсов:
    один градус широты — всегда примерно 111 км,
    один градус долготы — 111 км только на экваторе,
    а на широте 55.75 град — уже примерно 63 км.
Plain text

Система отсчёта, в которой спутниковый приёмник выдаёт координаты, называется WGS-84 (World Geodetic System 1984). Она описывает Землю как эллипсоид — слегка сплюснутый шар: экваториальный радиус 6378,137 км, полярный 6356,752 км; описание системы доступно по адресу https://en.wikipedia.org/wiki/World_Geodetic_System. Для наших задач — дистанции от метров до десятков километров — Землю достаточно считать шаром со средним радиусом \(R = 6\,371\,000\) метров: погрешность такой модели составляет доли процента и заведомо меньше ошибки самого спутникового приёмника, которая измеряется метрами.

С высотой связана важная развилка: существуют высота над уровнем моря (MSL, mean sea level) и относительная высота над точкой старта. Если домашняя точка находится на холме высотой 150 метров над уровнем моря, то относительная высота 20 метров соответствует 170 метрам MSL. MAVLink разделяет эти величины: сообщение GLOBAL_POSITION_INT несёт поле alt (высота над уровнем моря) и поле relative_alt (высота над домом), оба в миллиметрах; координаты lat и lon передаются в том же сообщении целыми числами — градусами, умноженными на \(10^7\). Целые числа используются потому, что протокол передаёт поля фиксированного размера, а целое значение не теряет точность при передаче; подробный разбор сообщений — в модуле M5. Справочник сообщений: https://mavlink.io/en/messages/common.html.

Дисциплина единиц из уроков K13–K14 сохраняется полностью: формулы этого урока работают с радианами, поэтому на входе каждой функции градусы переводятся через math.radians(), а результаты возвращаются в градусах через math.degrees(). Точка в коде представляется структурой GeoPoint — это dataclass из урока K09 с именованными полями lat_deg и lon_deg. Имена полей защищают от классической ошибки: порядок «широта, долгота» и «долгота, широта» перепутать в кортеже очень легко, а в именованных полях — трудно.

Осторожно на границах систем. Разные инструменты используют разный порядок координат: наш GeoPoint и MAVLink начинаются с широты, а формат GeoJSON и некоторые веб-карты — с долготы. При стыковке с внешним API проверяйте порядок в документации, иначе точка «уедет» на другой конец света.
↑ К оглавлению

5. Расстояние: большой круг и гаверсинус

Почему нельзя обойтись школьной теоремой Пифагора, применённой к разностям градусов? Потому что градусы — неодинаковые единицы длины. Один градус широты всегда соответствует примерно 111 километрам, а один градус долготы «сжимается» по мере движения к полюсам: его длина приблизительно равна \(111.32 \cdot \cos\varphi\) километров. На широте Москвы один градус долготы — это уже около 63 километров. Пифагор по градусам даст тем большую ошибку, чем сильнее широта места отличается от экватора и чем длиннее дистанция.

Правильная модель: кратчайший путь между двумя точками на сфере проходит по дуге большого круга — окружности, центр которой совпадает с центром сферы (экватор и любой меридиан — большие круги; параллель, кроме экватора, — нет). Расстояние по дуге большого круга вычисляет формула гаверсинуса. Канонический источник всех формул этого урока — классический справочник https://www.movable-type.co.uk/scripts/latlong.html. Пусть φ и λ — широта и долгота в радианах, индекс 1 — первая точка, индекс 2 — вторая, Δ — разность:

\[ a = \sin^2\!\left(\frac{\Delta\varphi}{2}\right) + \cos\varphi_1 \cdot \cos\varphi_2 \cdot \sin^2\!\left(\frac{\Delta\lambda}{2}\right) \]

\[ d = 2R \cdot \operatorname{atan2}\!\left(\sqrt{a},\ \sqrt{1-a}\right) \]

Словами: сначала вычисляется вспомогательная величина a — комбинация квадратов синусов половинных разностей координат, затем угловое расстояние через atan2 от двух корней, и наконец расстояние в метрах как произведение углового расстояния на радиус Земли. Именно эта форма, а не эквивалентная через арксинус, сохраняет точность на малых дистанциях — тех самых десятках и сотнях метров, на которых летает беспилотник.

Чистый код

# Пример: расстояние между двумя точками (фрагмент k15_task1_haversine.py)
import math
from dataclasses import dataclass

EARTH_RADIUS_M = 6371000.0

@dataclass
class GeoPoint:
    lat_deg: float
    lon_deg: float

def haversine_distance_m(p1: GeoPoint, p2: GeoPoint) -> float:
    lat1, lon1 = math.radians(p1.lat_deg), math.radians(p1.lon_deg)
    lat2, lon2 = math.radians(p2.lat_deg), math.radians(p2.lon_deg)
    d_lat = lat2 - lat1
    d_lon = lon2 - lon1
    a = (
        math.sin(d_lat / 2.0) ** 2
        + math.cos(lat1) * math.cos(lat2) * math.sin(d_lon / 2.0) ** 2
    )
    c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
    return EARTH_RADIUS_M * c
Python
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    import math
    from dataclasses import dataclass
    # → Только стандартная библиотека: этот урок не ставит новых пакетов.
    
    EARTH_RADIUS_M = 6371000.0
    # → Средний радиус Земли в метрах — одна константа на все формулы урока.
    # → Имя константы содержит единицу измерения: _M означает метры.
    
    @dataclass
    class GeoPoint:
        lat_deg: float
        lon_deg: float
    # → Dataclass из урока K09: именованные поля lat_deg и lon_deg
    # → исключают молчаливую путаницу порядка координат.
    # → Суффикс _deg фиксирует единицу: градусы, не радианы.
    
    def haversine_distance_m(p1: GeoPoint, p2: GeoPoint) -> float:
    # → Чистая функция: две точки на входе — расстояние в метрах на выходе.
    
        lat1, lon1 = math.radians(p1.lat_deg), math.radians(p1.lon_deg)
        lat2, lon2 = math.radians(p2.lat_deg), math.radians(p2.lon_deg)
    # → Перевод градусов в радианы на входе: вся тригонометрия ниже
    # → работает только с радианами (дисциплина уроков K13-K14).
    
        d_lat = lat2 - lat1
        d_lon = lon2 - lon1
    # → Разности координат. Периодичность синуса и косинуса делает
    # → формулу корректной даже при пересечении меридиана ±180 град.
    
        a = (
            math.sin(d_lat / 2.0) ** 2
            + math.cos(lat1) * math.cos(lat2) * math.sin(d_lon / 2.0) ** 2
        )
    # → Величина a из формулы гаверсинуса: ** 2 означает квадрат.
    # → Косинусы широт «сжимают» вклад долготы — ровно та поправка,
    # → которой не хватало наивной теореме Пифагора по градусам.
    
        c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
    # → Угловое расстояние c в радианах: форма через atan2
    # → численно устойчива на малых дистанциях.
    
        return EARTH_RADIUS_M * c
    # → Дуга = радиус × угол: расстояние в метрах.
    Python

Демонстрация практики печатает три контрольных числа: расстояние от центра Москвы до Балашихи — 22095 метров, расстояние одного градуса широты — 111195 метров (те самые «примерно 111 километров») и ноль для расстояния точки до самой себя. Отдельное полезное свойство: при чисто меридиональном смещении (долгота не меняется) гаверсинус сводится к точной формуле \(d = R \cdot \Delta\varphi\) — этот факт закрепляет тест.

↑ К оглавлению

6. Направление на точку: bearing

Второй вопрос после «как далеко?» — «в какую сторону?». Bearing (начальный азимут) — это угол от направления на север до направления на целевую точку, отсчитанный по часовой стрелке: 0° — цель на севере, 90° — на востоке, 180° — на юге, 270° — на западе. Обратите внимание: это в точности тот же компасный договор, который использовал курс (heading) в уроке K13. Поэтому результат bearing_deg() можно напрямую подать в heading_to_velocity_ned() — и получить вектор скорости в NED, ведущий к цели. Так математика двух уроков собирается в одну цепочку.

\[ \theta = \operatorname{atan2}\bigl(\sin\Delta\lambda \cdot \cos\varphi_2,\;\; \cos\varphi_1 \cdot \sin\varphi_2 - \sin\varphi_1 \cdot \cos\varphi_2 \cdot \cos\Delta\lambda \bigr) \]

Чистый код

# Пример: направление из точки в точку (фрагмент k15_task2_bearing.py)
import math
from k15_task1_haversine import GeoPoint

def bearing_deg(p1: GeoPoint, p2: GeoPoint) -> float:
    lat1, lon1 = math.radians(p1.lat_deg), math.radians(p1.lon_deg)
    lat2, lon2 = math.radians(p2.lat_deg), math.radians(p2.lon_deg)
    d_lon = lon2 - lon1
    y = math.sin(d_lon) * math.cos(lat2)
    x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(d_lon)
    return math.degrees(math.atan2(y, x)) % 360.0
Python
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    import math
    from k15_task1_haversine import GeoPoint
    # → Тип точки импортируется из модуля расстояния:
    # → одна структура данных на весь урок, без дублирования.
    
    def bearing_deg(p1: GeoPoint, p2: GeoPoint) -> float:
    # → Из p1 смотрим на p2; результат — градусы от севера по часовой стрелке.
    
        lat1, lon1 = math.radians(p1.lat_deg), math.radians(p1.lon_deg)
        lat2, lon2 = math.radians(p2.lat_deg), math.radians(p2.lon_deg)
        d_lon = lon2 - lon1
    # → Перевод в радианы и разность долгот — те же шаги, что в гаверсинусе.
    
        y = math.sin(d_lon) * math.cos(lat2)
        x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(d_lon)
    # → Два выражения канонической формулы. Их отношение — тангенс
    # → искомого угла, но делить нельзя: x может оказаться нулём.
    
        return math.degrees(math.atan2(y, x)) % 360.0
    # → atan2(y, x) из урока K14 возвращает угол в правильном квадранте,
    # → от -180 до +180 град; остаток от деления на 360 приводит
    # → ответ к компасному диапазону 0..360: -90 становится 270.
    Python

Два уточнения для честности картины. Первое: вдоль дуги большого круга направление постепенно меняется — формула возвращает начальный азимут. На дистанциях дрона (единицы километров) изменение составляет доли градуса и практики не касается. Второе: демонстрация печатает для цели строго на восток значение 89.96°, а не ровно 90°. Это не ошибка, а свойство сферы: дуга большого круга между двумя точками одной параллели слегка выгибается к полюсу, поэтому стартует чуть севернее чистого востока. Тест фиксирует направление с допуском и объясняет причину в комментарии.

↑ К оглавлению

7. Смещение точки на N метров

Третья задача — обратная к расстоянию и направлению: даны исходная точка, направление и дистанция; найти координаты конечной точки. Именно она превращает математику в маршрут: «отойти на 150 метров на северо-восток от дома», «построить квадрат 200 × 200 метров вокруг точки съёмки», «держать круг радиусом 50 метров вокруг цели». Формула конечной точки (destination point) также берётся из канонического справочника. В ней появляется угловое расстояние \(\delta = d / R\) — дистанция в метрах, выраженная в радианах дуги:

\[ \begin{aligned} \delta &= d / R \\ \varphi_2 &= \arcsin\bigl( \sin\varphi_1 \cos\delta + \cos\varphi_1 \sin\delta \cos\theta \bigr) \\ \lambda_2 &= \lambda_1 + \operatorname{atan2}\bigl( \sin\theta \sin\delta \cos\varphi_1,\;\; \cos\delta - \sin\varphi_1 \sin\varphi_2 \bigr) \end{aligned} \]

Чистый код

# Пример: смещение точки (фрагмент k15_task3_destination.py)
import math
from k15_task1_haversine import EARTH_RADIUS_M, GeoPoint

def destination_point(origin: GeoPoint, bearing_target_deg: float, distance_m: float) -> GeoPoint:
    lat1 = math.radians(origin.lat_deg)
    lon1 = math.radians(origin.lon_deg)
    theta = math.radians(bearing_target_deg)
    delta = distance_m / EARTH_RADIUS_M

    lat2 = math.asin(
        math.sin(lat1) * math.cos(delta)
        + math.cos(lat1) * math.sin(delta) * math.cos(theta)
    )
    lon2 = lon1 + math.atan2(
        math.sin(theta) * math.sin(delta) * math.cos(lat1),
        math.cos(delta) - math.sin(lat1) * math.sin(lat2),
    )
    return GeoPoint(math.degrees(lat2), math.degrees(lon2))
Python
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    import math
    from k15_task1_haversine import EARTH_RADIUS_M, GeoPoint
    # → Радиус Земли и тип точки берутся из первого модуля:
    # → все три формулы урока обязаны использовать одну константу.
    
    def destination_point(origin: GeoPoint, bearing_target_deg: float, distance_m: float) -> GeoPoint:
    # → Вход: исходная точка, направление в градусах, дистанция в метрах.
    # → Выход: новая точка — тоже GeoPoint, в градусах.
    
        lat1 = math.radians(origin.lat_deg)
        lon1 = math.radians(origin.lon_deg)
        theta = math.radians(bearing_target_deg)
    # → Все три угла переводятся в радианы до начала вычислений.
    
        delta = distance_m / EARTH_RADIUS_M
    # → Угловое расстояние: сколько радианов дуги «укладывается»
    # → в дистанцию. Сто метров — это примерно 1.57e-5 радиан.
    
        lat2 = math.asin(
            math.sin(lat1) * math.cos(delta)
            + math.cos(lat1) * math.sin(delta) * math.cos(theta)
        )
    # → Широта конечной точки. Аргумент asin здесь математически
    # → ограничен диапазоном от -1 до 1 и состоит из слагаемых
    # → с запасом, поэтому защемление, как в K14, не требуется;
    # → но тесты обязаны это поведение фиксировать.
    
        lon2 = lon1 + math.atan2(
            math.sin(theta) * math.sin(delta) * math.cos(lat1),
            math.cos(delta) - math.sin(lat1) * math.sin(lat2),
        )
    # → Долгота конечной точки: к исходной прибавляется приращение,
    # → вычисленное через atan2 в правильном квадранте.
    
        return GeoPoint(math.degrees(lat2), math.degrees(lon2))
    # → Возврат в градусах: интерфейс функции говорит с человеком,
    # → радианы остаются внутренней кухней.
    Python

Как убедиться, что формула перенесена в код верно? Тем же приёмом, что и в уроке K14: не сверять числа глазами, а проверить согласованность цепочки. Если сместить точку на 300 метров по направлению 45°, то обратный пересчёт обязан вернуть дистанцию 300 метров (гаверсинус) и направление 45° (bearing). Демо практики печатает именно эту обратную проверку, а тесты фиксируют её с допусками pytest.approx.

Плоское приближение — тоже рабочий инструмент. На дистанциях до километра иногда используют упрощение: сдвиг по широте равен \(d_N / R\), сдвиг по долготе — \(d_E / (R \cos\varphi)\), в радианах. Так устроена локальная система NED в окрестности дома. Мы сознательно используем точную формулу везде: она универсальна, не требует помнить границу применимости и стоит те же несколько строк кода.
↑ К оглавлению

8. Домашняя позиция и простейшая геозона

Домашняя позиция (home position, «дом») — точка, которую автопилот запоминает при первом уверенном определении позиции и при взведении моторов. К ней аппарат возвращается по команде RTL (Return To Launch — «возврат на точку старта»), она же служит началом отсчёта относительной высоты. В симуляторе SITL домашняя позиция создаётся при запуске; на реальном аппарате — на месте старта.

Геозона (geofence) — виртуальная граница, за которую аппарату вылетать запрещено. В ArduPilot существует полноценный механизм геозоны (geofence) с группой параметров, имена которых начинаются с FENCE_ (например, FENCE_ENABLE, FENCE_RADIUS, FENCE_ALT_MAX); настраивать их мы будем в модулях M4 и M6. Учебная версия повторяет логику простейшего случая: разрешённая зона — круг заданного радиуса вокруг домашней точки с потолком высоты. Функция проверки возвращает один из трёх статусов: "OK", "OUT_OF_RADIUS" или "ABOVE_CEILING". Настоящая система по результату проверки запускает защитное действие — возврат домой или посадку; этот механизм называется failsafe, и вы знакомы с ним со вводного урока K00.

Чистый код

# Пример: простейшая геозона (фрагмент k15_task4_geofence.py)
from k15_task1_haversine import GeoPoint, haversine_distance_m

def geofence_status(position: GeoPoint, alt_m: float, home: GeoPoint,
                    radius_m: float, max_alt_m: float) -> str:
    distance_m = haversine_distance_m(home, position)
    if distance_m > radius_m:
        return "OUT_OF_RADIUS"
    if alt_m > max_alt_m:
        return "ABOVE_CEILING"
    return "OK"
Python
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    from k15_task1_haversine import GeoPoint, haversine_distance_m
    # → Геозона собирается из готовых кирпичей урока: точка и расстояние.
    
    def geofence_status(position: GeoPoint, alt_m: float, home: GeoPoint,
                        radius_m: float, max_alt_m: float) -> str:
    # → Вход: текущая позиция, относительная высота, домашняя точка,
    # → разрешённый радиус и потолок высоты. Выход — строка-статус.
    
        distance_m = haversine_distance_m(home, position)
    # → Расстояние от дома вычисляется гаверсинусом из модуля задачи 15.1.
    
        if distance_m > radius_m:
            return "OUT_OF_RADIUS"
    # → Первая проверка: выход за горизонтальную границу.
    # → Порядок проверок зафиксирован и описан в docstring:
    # → сначала радиус, затем потолок.
    
        if alt_m > max_alt_m:
            return "ABOVE_CEILING"
    # → Вторая проверка: превышение потолка высоты.
    
        return "OK"
    # → Обе границы соблюдены — полёт в разрешённой зоне.
    # → Строковые статусы — та же идея меток OK/LOW/CRITICAL из K11.
    Python
Зачем вам это в курсе. Функция geofence_status() — зародыш логики безопасности, которая будет расти вместе с курсом: в модуле M6 она встретится с настоящими параметрами FENCE_* автопилота, а в капстоун-проектах модуля M12 станет частью наземного сервиса, который следит за аппаратом во время миссии. Математика зоны (расстояние от дома) уже написана и протестирована вами.
↑ К оглавлению

9. Тесты геодезического модуля

Тесты урока продолжают стратегию K14: вместо «сверить числа глазами» проверяются опорные значения, свойства функций и согласованность цепочки преобразований. Файл k15_task5_test_geo.py содержит двенадцать тестов трёх классов.

  • Опорные значения: расстояние от точки до самой себя равно нулю; направление строго на север равно 0°, строго на юг — 180°.
  • Свойства: расстояние симметрично (порядок точек не важен); смещение строго на север увеличивает широту и не меняет долготу; один градус широты равен \(R \cdot \Delta\varphi\) в радианах.
  • Согласованность цепочки: сместили точку — гаверсинус вернул исходную дистанцию, bearing вернул исходное направление; статусы геозоны соответствуют трём сценариям нарушения и одному корректному.

Чистый код

# Пример: тесты свойств и согласованности (фрагмент k15_task5_test_geo.py)
import math
import pytest
from k15_task1_haversine import EARTH_RADIUS_M, GeoPoint, haversine_distance_m
from k15_task3_destination import destination_point

HOME = GeoPoint(55.7558, 37.6173)

def test_one_degree_latitude():
    north = GeoPoint(HOME.lat_deg + 1.0, HOME.lon_deg)
    expected = EARTH_RADIUS_M * math.radians(1.0)
    assert haversine_distance_m(HOME, north) == pytest.approx(expected, rel=1e-9)

def test_destination_round_trip_distance():
    target = destination_point(HOME, 45.0, 300.0)
    assert haversine_distance_m(HOME, target) == pytest.approx(300.0, rel=1e-6)
Python
  • Построчный разбор

    Ниже приведён тот же фрагмент с пояснением после каждой смысловой строки. Строки, которые начинаются с последовательности символов «# →», содержат комментарий преподавателя. Эти строки не являются обязательной частью программы.

    import math
    import pytest
    from k15_task1_haversine import EARTH_RADIUS_M, GeoPoint, haversine_distance_m
    from k15_task3_destination import destination_point
    # → Тесты импортируют функции из модулей урока — схема K11:
    # → логика отдельно, проверки отдельно.
    
    HOME = GeoPoint(55.7558, 37.6173)
    # → Общая точка отсчёта тестов: координаты центра Москвы.
    
    def test_one_degree_latitude():
        north = GeoPoint(HOME.lat_deg + 1.0, HOME.lon_deg)
        expected = EARTH_RADIUS_M * math.radians(1.0)
        assert haversine_distance_m(HOME, north) == pytest.approx(expected, rel=1e-9)
    # → Опорное значение, выведенное из математики: чистое меридиональное
    # → смещение сводит гаверсинус к точной формуле дуги R·Δφ.
    # → rel=1e-9: совпадение обязано быть почти машинно-точным.
    
    def test_destination_round_trip_distance():
        target = destination_point(HOME, 45.0, 300.0)
        assert haversine_distance_m(HOME, target) == pytest.approx(300.0, rel=1e-6)
    # → Согласованность цепочки: сместились на 300 метров —
    # → обратное расстояние обязано вернуть 300 метров.
    # → Тест ловит перепутанные знаки и оси в обеих формулах сразу.
    Python

Отдельно прокомментируем выбор допусков. Для расстояний используется относительный допуск rel: он масштабируется вместе с величиной. Для направлений в градусах используется абсолютный допуск abs: например, проверка «на восток» написана как pytest.approx(90.0, abs=0.5), потому что истинное значение 89.96 — не ошибка кода, а физика большого круга, и тест обязан это учитывать, оставаясь при этом строгим к настоящим поломкам.

↑ К оглавлению

10. Типичные ошибки

ОшибкаЧто происходитКак следует рассуждать
Градусы поданы в тригонометрию без перевода Расстояния и направления неверны на порядки math.radians() на входе каждой функции, math.degrees() на выходе к человеку
Теорема Пифагора по разностям градусов Ошибка растёт с широтой и дистанцией: градус долготы «сжимается» к полюсам Для дистанций любой длины используйте гаверсинус
Перепутан порядок координат: (lon, lat) вместо (lat, lon) Точка «уезжает» в другую часть света Структура GeoPoint с именованными полями; при стыковке с API читайте документацию — GeoJSON начинается с долготы
Высота над уровнем моря смешана с относительной Ошибка в десятки и сотни метров: потолок геозоны срабатывает не там В MAVLink это разные поля: alt и relative_alt; фиксируйте систему отсчёта в имени переменной
Ручные вычисления долготы при пересечении меридиана ±180° Знаки и расстояния «прыгают» Канонические формулы защищены периодичностью синуса и косинуса; не изобретайте плоскую арифметику долгот
Тест направления на восток с точным равенством 90.0 Тест падает: истинное значение 89.96 из-за выгиба большого круга Допуск abs=0.5 и комментарий о причине
Координаты MAVLink поделены не на тот масштаб Позиция оказывается в тысячах раз больше ожидаемой Поля lat и lon сообщения GLOBAL_POSITION_INT — градусы, умноженные на 10⁷; делите на 10⁷ (разбор в M5)
↑ К оглавлению

11. Практика

Практика урока состоит из пяти файлов: четыре модуля геодезической математики и один файл тестов. Новые пакеты не потребуются: урок использует только стандартную библиотеку (math, dataclasses) и уже установленный pytest. Порядок работы: создайте файлы задач 15.1–15.5 в каталоге code/M01_python, выполните прогоны из задачи 15.6 и сохраните исходники в Git (задача 15.7). В карточках в аккордеонах лежит полный текст каждого файла; он совпадает с содержимым репозитория курса.

Задача 15.1. Расстояние (haversine)

Файл практики: k15_task1_haversine.py.

Создайте базовый модуль геодезии: структуру GeoPoint на dataclass, константу EARTH_RADIUS_M и функцию расстояния haversine_distance_m() с канонической формулой и ссылкой на источник в комментарии.

Ожидаемый результат. Демо печатает расстояние Москва → Балашиха 22095 м, один градус широты 111195 м и 0.00 м для расстояния точки до самой себя.

  • Полный текст файла k15_task1_haversine.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k15_task1_haversine.py в проекте PyCharm (виртуальное окружение) и запустите его из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k15_task1_haversine.py
    # Цель: расстояние между двумя точками по широте и долготе — формула гаверсинуса.
    # Канонический источник формул:
    #   https://www.movable-type.co.uk/scripts/latlong.html
    # Запуск из каталога code/M01_python:
    #   python k15_task1_haversine.py
    
    import math
    from dataclasses import dataclass
    
    EARTH_RADIUS_M = 6371000.0  # средний радиус Земли, метры
    
    
    @dataclass
    class GeoPoint:
        """Географическая точка: широта и долгота в градусах.
    
        Dataclass намеренно хранит поля под именами lat_deg и lon_deg,
        чтобы порядок «широта, долгота» нельзя было перепутать молча.
        """
    
        lat_deg: float
        lon_deg: float
    
    
    def haversine_distance_m(p1: GeoPoint, p2: GeoPoint) -> float:
        """Расстояние между двумя точками по дуге большого круга, метры."""
        lat1, lon1 = math.radians(p1.lat_deg), math.radians(p1.lon_deg)
        lat2, lon2 = math.radians(p2.lat_deg), math.radians(p2.lon_deg)
        d_lat = lat2 - lat1
        d_lon = lon2 - lon1
        a = (
            math.sin(d_lat / 2.0) ** 2
            + math.cos(lat1) * math.cos(lat2) * math.sin(d_lon / 2.0) ** 2
        )
        c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
        return EARTH_RADIUS_M * c
    
    
    if __name__ == "__main__":
        moscow = GeoPoint(55.7558, 37.6173)      # Москва, центр
        balashikha = GeoPoint(55.8117, 37.9564)  # Балашиха
    
        d = haversine_distance_m(moscow, balashikha)
        print(f"Москва -> Балашиха: {d:.0f} м")
    
        one_deg_north = GeoPoint(moscow.lat_deg + 1.0, moscow.lon_deg)
        print(f"один градус широты: {haversine_distance_m(moscow, one_deg_north):.0f} м")
        print(f"та же точка: {haversine_distance_m(moscow, moscow):.2f} м")
    Python

Задача 15.2. Направление (bearing)

Файл практики: k15_task2_bearing.py.

Создайте модуль направления: функция bearing_deg() вычисляет начальный азимут через math.atan2() и приводит результат к компасному диапазону операцией остатка от деления на 360. Демо печатает направления на четыре стороны света и на Балашиху.

Ожидаемый результат. Демо печатает 0.00 град на север, 89.96 град на восток, 180.00 град на юг, 270.04 град на запад.

  • Полный текст файла k15_task2_bearing.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k15_task2_bearing.py в проекте PyCharm (виртуальное окружение) и запустите его из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k15_task2_bearing.py
    # Цель: направление (bearing) из одной точки на другую — начальный азимут.
    # Канонический источник формул:
    #   https://www.movable-type.co.uk/scripts/latlong.html
    # Запуск из каталога code/M01_python:
    #   python k15_task2_bearing.py
    
    import math
    
    from k15_task1_haversine import GeoPoint
    
    
    def bearing_deg(p1: GeoPoint, p2: GeoPoint) -> float:
        """Начальное направление из p1 на p2: градусы от севера по часовой стрелке, 0..360.
    
        Тот же компасный договор, что и у курса (heading) в уроке K13,
        поэтому результат можно подавать в heading_to_velocity_ned().
        """
        lat1, lon1 = math.radians(p1.lat_deg), math.radians(p1.lon_deg)
        lat2, lon2 = math.radians(p2.lat_deg), math.radians(p2.lon_deg)
        d_lon = lon2 - lon1
        y = math.sin(d_lon) * math.cos(lat2)
        x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(d_lon)
        return math.degrees(math.atan2(y, x)) % 360.0
    
    
    if __name__ == "__main__":
        home = GeoPoint(55.7558, 37.6173)
        north = GeoPoint(55.8558, 37.6173)
        east = GeoPoint(55.7558, 37.7173)
        south = GeoPoint(55.6558, 37.6173)
        west = GeoPoint(55.7558, 37.5173)
        balashikha = GeoPoint(55.8117, 37.9564)
    
        for title, target in (("север", north), ("восток", east), ("юг", south), ("запад", west)):
            print(f"направление на {title}: {bearing_deg(home, target):6.2f} град")
        print(f"направление Москва -> Балашиха: {bearing_deg(home, balashikha):6.2f} град")
    Python

Задача 15.3. Смещение точки

Файл практики: k15_task3_destination.py.

Создайте модуль смещения: функция destination_point() возвращает точку на заданной дистанции в заданном направлении канонической формулой конечной точки. Демо выполняет обратную проверку цепочкой: сместили точку — измерили расстояние и направление обратно.

Ожидаемый результат. Демо печатает обратную проверку: смещение на 100 м на север восстанавливается как 100.00 м и 0.00 град; смещение на 300 м на северо-восток — как 300.00 м и 45.00 град.

  • Полный текст файла k15_task3_destination.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k15_task3_destination.py в проекте PyCharm (виртуальное окружение) и запустите его из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k15_task3_destination.py
    # Цель: смещение точки на заданное расстояние в заданном направлении —
    #       формула конечной точки (destination point).
    # Канонический источник формул:
    #   https://www.movable-type.co.uk/scripts/latlong.html
    # Запуск из каталога code/M01_python:
    #   python k15_task3_destination.py
    
    import math
    
    from k15_task1_haversine import EARTH_RADIUS_M, GeoPoint, haversine_distance_m
    from k15_task2_bearing import bearing_deg
    
    
    def destination_point(origin: GeoPoint, bearing_target_deg: float, distance_m: float) -> GeoPoint:
        """Точка на расстоянии distance_m от origin в направлении bearing_target_deg (градусы)."""
        lat1 = math.radians(origin.lat_deg)
        lon1 = math.radians(origin.lon_deg)
        theta = math.radians(bearing_target_deg)
        delta = distance_m / EARTH_RADIUS_M  # угловое расстояние в радианах
    
        lat2 = math.asin(
            math.sin(lat1) * math.cos(delta)
            + math.cos(lat1) * math.sin(delta) * math.cos(theta)
        )
        lon2 = lon1 + math.atan2(
            math.sin(theta) * math.sin(delta) * math.cos(lat1),
            math.cos(delta) - math.sin(lat1) * math.sin(lat2),
        )
        return GeoPoint(math.degrees(lat2), math.degrees(lon2))
    
    
    if __name__ == "__main__":
        home = GeoPoint(55.7558, 37.6173)
        cases = (
            ("север", 0.0, 100.0),
            ("восток", 90.0, 100.0),
            ("северо-восток по диагонали", 45.0, 300.0),
        )
        for title, bearing, dist in cases:
            target = destination_point(home, bearing, dist)
            back_dist = haversine_distance_m(home, target)
            back_bearing = bearing_deg(home, target)
            print(
                f"{title}: lat={target.lat_deg:.7f} lon={target.lon_deg:.7f} | "
                f"обратная проверка: {back_dist:.2f} м, {back_bearing:.2f} град"
            )
    Python

Задача 15.4. Простейшая геозона

Файл практики: k15_task4_geofence.py.

Создайте модуль безопасности: функция geofence_status() проверяет расстояние от домашней точки (радиус) и относительную высоту (потолок) и возвращает один из трёх строковых статусов. Демо разбирает четыре сценария полёта.

Ожидаемый результат. Демо печатает статусы OK, OUT_OF_RADIUS для вылета на 150 м при радиусе 100 м и ABOVE_CEILING для высоты 60 м при потолке 50 м.

  • Полный текст файла k15_task4_geofence.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k15_task4_geofence.py в проекте PyCharm (виртуальное окружение) и запустите его из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k15_task4_geofence.py
    # Цель: простейшая геозона (geofence) — проверка, что аппарат не покинул
    #       разрешённую зону: расстояние от домашней точки и потолок высоты.
    # Запуск из каталога code/M01_python:
    #   python k15_task4_geofence.py
    
    from k15_task1_haversine import GeoPoint, haversine_distance_m
    from k15_task3_destination import destination_point
    
    
    def geofence_status(
        position: GeoPoint,
        alt_m: float,
        home: GeoPoint,
        radius_m: float,
        max_alt_m: float,
    ) -> str:
        """Вернуть 'OK', 'OUT_OF_RADIUS' или 'ABOVE_CEILING'.
    
        alt_m — высота относительно домашней точки (упрощение: в MAVLink
        относительная высота и высота над уровнем моря — разные поля).
        Порядок проверок зафиксирован: сначала радиус, затем потолок.
        """
        distance_m = haversine_distance_m(home, position)
        if distance_m > radius_m:
            return "OUT_OF_RADIUS"
        if alt_m > max_alt_m:
            return "ABOVE_CEILING"
        return "OK"
    
    
    if __name__ == "__main__":
        home = GeoPoint(55.7558, 37.6173)
        radius_m = 100.0
        max_alt_m = 50.0
    
        scenarios = (
            ("висение на 20 м в 50 м от дома", destination_point(home, 0.0, 50.0), 20.0),
            ("вылетел за радиус на 150 м", destination_point(home, 0.0, 150.0), 20.0),
            ("поднялся на 60 м в пределах радиуса", destination_point(home, 0.0, 50.0), 60.0),
            ("у дома, высота 0", home, 0.0),
        )
        for title, position, alt in scenarios:
            status = geofence_status(position, alt, home, radius_m, max_alt_m)
            dist = haversine_distance_m(home, position)
            print(f"{title}: dist {dist:6.1f} м, alt {alt:4.1f} м -> {status}")
    Python

Задача 15.5. Тесты геодезии

Файл практики: k15_task5_test_geo.py.

Создайте файл тестов: три опорных значения расстояния, три проверки направления, три проверки смещения точки и три сценария геозоны — всего двенадцать тестов с допусками pytest.approx (относительный rel для метров, абсолютный abs для градусов).

Ожидаемый результат. Команда python -m pytest k15_task5_test_geo.py -v показывает двенадцать строк PASSED и итог «12 passed».

  • Полный текст файла k15_task5_test_geo.py

    Скопируйте приведённое ниже содержимое в файл code/M01_python/k15_task5_test_geo.py в проекте PyCharm (виртуальное окружение) и запустите тесты командой python -m pytest k15_task5_test_geo.py -v из каталога code/M01_python. Ниже приведён полный учебный текст файла; он совпадает с файлом в репозитории курса.

    # k15_task5_test_geo.py
    # Тесты геодезического модуля K15: гаверсинус, bearing, смещение точки, геозона.
    # Запуск из каталога code/M01_python:
    #   python -m pytest k15_task5_test_geo.py -v
    
    import math
    
    import pytest
    
    from k15_task1_haversine import EARTH_RADIUS_M, GeoPoint, haversine_distance_m
    from k15_task2_bearing import bearing_deg
    from k15_task3_destination import destination_point
    from k15_task4_geofence import geofence_status
    
    HOME = GeoPoint(55.7558, 37.6173)
    
    
    # --- гаверсинус ------------------------------------------------------------
    
    
    def test_same_point_distance_is_zero():
        assert haversine_distance_m(HOME, HOME) == pytest.approx(0.0, abs=1e-6)
    
    
    def test_one_degree_latitude():
        north = GeoPoint(HOME.lat_deg + 1.0, HOME.lon_deg)
        expected = EARTH_RADIUS_M * math.radians(1.0)
        assert haversine_distance_m(HOME, north) == pytest.approx(expected, rel=1e-9)
    
    
    def test_distance_is_symmetric():
        other = GeoPoint(55.8117, 37.9564)
        assert haversine_distance_m(HOME, other) == pytest.approx(
            haversine_distance_m(other, HOME)
        )
    
    
    # --- bearing ----------------------------------------------------------------
    
    
    def test_bearing_north_is_zero():
        north = GeoPoint(HOME.lat_deg + 0.1, HOME.lon_deg)
        assert bearing_deg(HOME, north) == pytest.approx(0.0, abs=1e-9)
    
    
    def test_bearing_south_is_180():
        south = GeoPoint(HOME.lat_deg - 0.1, HOME.lon_deg)
        assert bearing_deg(HOME, south) == pytest.approx(180.0, abs=1e-9)
    
    
    def test_bearing_east_near_90():
        east = GeoPoint(HOME.lat_deg, HOME.lon_deg + 0.1)
        # Большой круг слегка «выгибается» к полюсу, поэтому курс на точку
        # той же широты чуть меньше 90 град; на малых расстояниях — доли градуса.
        assert bearing_deg(HOME, east) == pytest.approx(90.0, abs=0.5)
    
    
    # --- смещение точки -----------------------------------------------------------
    
    
    def test_destination_round_trip_distance():
        target = destination_point(HOME, 45.0, 300.0)
        assert haversine_distance_m(HOME, target) == pytest.approx(300.0, rel=1e-6)
    
    
    def test_destination_north_increases_latitude():
        target = destination_point(HOME, 0.0, 100.0)
        assert target.lat_deg > HOME.lat_deg
        assert target.lon_deg == pytest.approx(HOME.lon_deg, abs=1e-9)
    
    
    def test_destination_bearing_consistency():
        target = destination_point(HOME, 137.0, 500.0)
        assert bearing_deg(HOME, target) == pytest.approx(137.0, abs=0.1)
    
    
    # --- геозона ----------------------------------------------------------------
    
    
    def test_geofence_ok():
        inside = destination_point(HOME, 90.0, 50.0)
        assert geofence_status(inside, 20.0, HOME, 100.0, 50.0) == "OK"
    
    
    def test_geofence_out_of_radius():
        outside = destination_point(HOME, 90.0, 150.0)
        assert geofence_status(outside, 20.0, HOME, 100.0, 50.0) == "OUT_OF_RADIUS"
    
    
    def test_geofence_above_ceiling():
        inside = destination_point(HOME, 90.0, 50.0)
        assert geofence_status(inside, 60.0, HOME, 100.0, 50.0) == "ABOVE_CEILING"
    Python

Задача 15.6. Прогоны демо и тестов (обязательно выполнить)

Выполните команды по порядку из каталога code/M01_python в терминале с активным виртуальным окружением.

# Каталог: code/M01_python (venv активен, новых пакетов не требуется)
python k15_task1_haversine.py
python k15_task2_bearing.py
python k15_task3_destination.py
python k15_task4_geofence.py
python -m pytest k15_task5_test_geo.py -v
Plain text

Ожидаемый результат. Ключевые строки вывода демо (сверьте значения):

task1: Москва -> Балашиха: 22095 м
       один градус широты: 111195 м
       та же точка: 0.00 м

task2: направление на север:   0.00 град
       направление на восток:  89.96 град
       направление на юг: 180.00 град
       направление на запад: 270.04 град
       направление Москва -> Балашиха:  73.52 град

task3: север: lat=55.7566993 lon=37.6173000 | обратная проверка: 100.00 м, 0.00 град
       восток: lat=55.7558000 lon=37.6188982 | обратная проверка: 100.00 м, 90.00 град
       северо-восток по диагонали: lat=55.7577077 lon=37.6206904 | обратная проверка: 300.00 м, 45.00 град

task4: висение на 20 м в 50 м от дома: dist   50.0 м, alt 20.0 м -> OK
       вылетел за радиус на 150 м: dist  150.0 м, alt 20.0 м -> OUT_OF_RADIUS
       поднялся на 60 м в пределах радиуса: dist   50.0 м, alt 60.0 м -> ABOVE_CEILING

tests: 12 passed
Plain text

Значение 89.96 град для направления строго на восток объяснено в разделе 6: дуга большого круга между точками одной параллели слегка выгибается к полюсу.

Задача 15.7. Сохранение в Git

Добавьте в репозиторий пять файлов урока и сделайте коммит — последний в модуле M2.

# Из корня репозитория курса
git add code/M01_python/k15_task1_haversine.py code/M01_python/k15_task2_bearing.py code/M01_python/k15_task3_destination.py code/M01_python/k15_task4_geofence.py code/M01_python/k15_task5_test_geo.py
git commit -m "K15: geo haversine, bearing, destination point, geofence, 12 tests"
Plain text

Ожидаемый результат. Команда git status показывает чистое рабочее дерево по файлам k15_*, в истории появился коммит урока K15.

↑ К оглавлению

12. Проверьте себя

  1. Что измеряют широта и долгота и в каких диапазонах они изменяются?
  2. Что такое WGS-84 и почему для наших задач Землю достаточно считать шаром среднего радиуса?
  3. Почему один градус долготы соответствует разным расстояниям на разных широтах и как это связано с отказом от теоремы Пифагора по градусам?
  4. Что такое большой круг и какую величину вычисляет формула гаверсинуса?
  5. В какой договорённости функция bearing_deg() возвращает направление и в какую функцию урока K13 его можно подать напрямую?
  6. Почему направление на точку той же широты равно 89.96°, а не ровно 90°?
  7. Какую задачу решает функция destination_point()? Приведите два примера из планирования миссий.
  8. Что такое домашняя позиция и как она связана с командой RTL и относительной высотой?
  9. Какие статусы возвращает наша геозона и какие условия за ними стоят?
  10. Почему сообщение GLOBAL_POSITION_INT передаёт координаты целыми числами, умноженными на 10⁷, и в каких единицах приходят высоты?
↑ К оглавлению

13. Чек-лист самопроверки

  • Я создал все пять файлов практики в каталоге code/M01_python.
  • Демо задачи 15.1 напечатало 22095 м для пары Москва — Балашиха и 111195 м для одного градуса широты.
  • Я могу объяснить, почему направление строго на восток получилось 89.96°, а не 90.00°.
  • Демо задачи 15.3 показало обратную проверку: дистанция и направление восстанавливаются после смещения.
  • Демо задачи 15.4 показало все три статуса геозоны: OK, OUT_OF_RADIUS, ABOVE_CEILING.
  • Команда python -m pytest k15_task5_test_geo.py -v завершается результатом 12 passed.
  • Я могу объяснить разницу высоты над уровнем моря и относительной высоты и назвать поля GLOBAL_POSITION_INT.
  • Я сделал git commit с файлами урока K15.
Результат. Когда все пункты закрыты, напишите ментору: K15 чек-лист закрыт. Это последний конспект модуля M2 «Математика дрона»: после него ментор зафиксирует закрытие модуля и выдаст K16 — первый урок модуля M3 «Протоколы и интерфейсы».
↑ К оглавлению

14. Что дальше

  • Предыдущий урок: K14 · Углы Эйлера, кватернионы и DCM
  • Текущий урок: K15 · Геокоординаты: lat/lon/alt, расстояние haversine, направление bearing
  • Следующий урок: K16 · Слои связи «для дронов» — начало модуля M3 «Протоколы и интерфейсы»
  • Модуль M3 пройдёт путь от физики байтов на проводе до UDP-сокетов и ручного разбора бинарных кадров; после лабораторной работы K22 сдаётся гейт G2.
↑ К оглавлению

 

 

Пятница, 06 ноября 2026
k15. Математика дрона: геокоординаты — lat/lon/alt, расстояние haversine, направление bearing