Компас, указывающий не на север, или как мне пригодилась тригонометрия

Привет, Хабр!

Я занимаюсь разработкой под iOS и недавно передо мной встала задача – создать компас, который указывает направление не на север, а на определенную точку земли. Это конечно не совсем компас, но за неимением лучшего названия, буду называть его так.

Наша компания занималась разработкой мобильного помощника для мусульман, частью которого и был этот самый компас. Его задача – указывать направление на Мекку. Подобная функция имеется практически в каждом подобном приложении, так что идея далеко не новая, однако над реализацией пришлось поломать голову.

Написание обычного компаса на objective-c – задача несложная, и можно найти множество примеров его реализации. Нужно с помощью класса CLLocationManager получить текущее значение heading, которое показывает, на сколько градусов текущее направление устройства отклонено от направления на север (далее просто heading). Затем взять этот угол, умножить на -1, перевести в радианы, и повернуть картинку с компасом на получившейся угол. То есть, на сколько повернуто устройство, на столько же мы поворачиваем картинку с копмасом, только в обратном направлении.

В моем случае, мне тоже нужно было повернуть картинку на определенный угол, только отклонение вычислять не относительно севера, а относительно Мекки. Таким образом задача сводилась к вычислению угла, который показывает, насколько текущее направление устройства отклонено от направления на Мекку. При этом, входными параметрами являлись текущие координаты устройства, координаты Мекки и значение heading. Поразмыслив немного, я решил, что неплохо было бы представить все это ввиде прямоугольной системы координат, с двумя точками на ней, где ось абсцисс – это значения долготы, а ось ординат – широты.



И действительно, в таком виде задача выглядит намного проще! Теперь можно свести все это к вычислению одного из углов прямоугольного треугольника. Нас интересует угол, прилежащий к катету, который параллелен оси ординат (В). Итак, мы легко можем вычислить стороны А и В, а затем и сторону С. Угол α можно вычислить через арксинус отношения стороны А к стороне С.



Отлично! Но полученный угол еще не является решением поставленной задачи. Нужно произвести некоторые дополнительные вычисления, причем они зависят от того, в какой четверти относительно Мекки находится пользователь. Мы хотим вычислить угол (обозначен оранжевым цветом) между направлением на Мекку и направлениям на север при текущем местоположении. Так, в ситуации № 1 (если пользователь к северо-востоку от Мекки), нужно прибавить к углу α 180 градусов. В ситуации № 2 (к юго-востоку от Мекки) – из 360 вычесть угол β. В ситуации № 3 (к юго-западу от Мекки) угол γ уже является искомым углом. В ситуации № 4 (к северо-западу от Мекки) нужно из 180 вычесть угол δ.



Итак, мы вычислили угол между направлением на Мекку и направлениям на север. Так как устройство уже отклонено на какой-то угол относительно севера (heading), нужно из heading вычесть вычисленный нами угол. Теперь наконец мы имеем угол, на который нам нужно повернуть нашу картинку с компасом (его еще нужно умножить на -1 и перевести в радианы).

Вот пример метода, который получает координаты и значение heading, и возвращает нужный нам угол:

- (double)angleFromLocation:(CLLocationCoordinate2D)coordinate andHeading:(double)heading
{
    double ourLatitude = coordinate.latitude;
    double ourLongitude = coordinate.longitude;
    
    // вычисляем катеты и гипотенузу
    double cathetusA = ABS(ourLongitude - MEKKA_LON);
    double cathetusB = ABS(ourLatitude - MEKKA_LAT);
    double hypotenuse = sqrt(cathetusA * cathetusA + cathetusB * cathetusB);
    
    // вычисляем угол α
    double angle1 = asin(cathetusA/hypotenuse) * 180/M_PI;
    double angle2; // насколько направление на Мекку отклонено от направления на север
    double angle3; // результат
    
    // перебираем все возможные варианты расположения относительно Мекки, и производим дополнительные вычисления
    if (ourLatitude > MEKKA_LAT) {
        if (ourLongitude > MEKKA_LON) {
            angle2 = 180.0 + angle1;
        }
        else if (ourLongitude < MEKKA_LON) {
            angle2 = 180.0 - angle1;
        }
        else {
            angle2 = 180.0;
        }
    }
    else if (ourLatitude < MEKKA_LAT) {
        if (ourLongitude > MEKKA_LON) {
            angle2 = 360.0 - angle1;
        }
        else if (ourLongitude < MEKKA_LON) {
            angle2 = angle1;
        }
        else {
            angle2 = 0.0;
        }
    }
    else {
        if (ourLongitude > MEKKA_LON) {
            angle2 = 270.0;
        }
        else if (ourLon < MEKKA_LON) {
            angle2 = 90.0;
        }
        else {
            angle2 = 0.0;
        }
    }
    
    angle3 = heading - angle2; // насколько утсройство отклонено от направления на Мекку
    angle3 = -angle3 * M_PI / 180.0f; // на какой угол нужно повернуть картинку компаса
    return angle3;
}


В итоге все работает! Можно использовать координаты различных городов мира и убедиться, что направление вычисляется правильно. Добавив пару дизайнерских изысков, получили довольно интересный компас:



Мне интересно, существуют ли иные способы решения этой задачи? Я надеюсь, что пост получился интересным и полезным.

Буду рад вашим комментариям.
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 22

    +12
    Фундаментальный труд
      +7
      Эээ… ошибок в определении направления — туча.

      Начну с того, что в точке вблизи западной границы вашей картинки «компас» должен показывать на запад (Земля-то круглая!), а ваши вычисления покажут на восток. Аналогичная ситуация случается у полюсов — так, если кратчайший путь до Мекки будет проходить через северный полюс, ваша программа опять обманет с направлением.

      Ну и предположение, что кратчайший путь в декартовой проекции (эта та, которую вы используете) выглядит как прямая — тоже странно. А нет прямой — нет и треугольника, которым вы пользуетесь.
        +4
        Через векторы такое делается же, пусть М — точка, где находится Мекка, А — текущая точка, тогда стрелка компаса должна будет под тем же углом, что и вектор МА
        МА = (Хм-Ха, Ум-Уа) (координаты мекки — координаты текущей точки)
        Угол же вектора МА тоже надо от чего-то мерять, будем от оси У, тогда возьмем вектор а{0,1}
        MA * a = |MA|*|a|*cos(alpha) = Xма*1 + Yма * 0 (скалярное произведение)
        cos(alpha) = Xma/|MA|
        alpha = arccos(Xma/(Xma^2+Yma^2))

        Как-то так, формулы не проверял. Ну и то, что земля не плоскость и к любой точке можно попасть из противоположных сторон создает свои проблемы.
        +20
        А теперь, как такие задачи надо решать. Во-первых, тригонометрию надо забыть. В многих ее формулах полно сингулярностей, от которых желательно избавляться, чтобы не получилось как в историях про знаменитые глюки американских военных самолетов.
        Точнее, полностью избавиться от нее, конечно же, не получится — но формулы будут использоваться только простейшие.

        Во-вторых, никаких предположений вида «просто возьмем и нарисуем так». Все рисунки должны иметь смысл (вы вот проверяли, как будет выглядеть гипотенуза в вашем треугольнике на поверхности планеты?)

        Считать можно двумя способами: в векторах и в кватернионах. Второй способ мне в школе не рассказывали, даже на «олимпиадном» спецкурсе, поэтому я буду использовать первый как более привычный.

        Возьмем систему геоцентрическую систему отсчета: начало координат — в центре Земли, оси x и y — в плоскости экватора, ось z смотрит на северный полюс. Также для первого раза примем Землю шарообразной — все одно получится точнее, чем у вас.

        Сначала построим векторы a и b — это будут радиус-векторы текущего положения и Мекки. (Тут первый раз будет применена «тригонометрия», в соответствии с известными формулами перехода от сферической системы координат к декартовой).

        Кратчайший путь — это дуга большого круга, большой круг — это сечение шара плоскостью, проходящей через центр. Искомое направление — это касательная к кратчайшему пути, она, очевидно, лежит в той же самой плоскости сечения. Если построить чертеж в этой самой плоскости, то будет видно, что искомое направление — это проекция вектора b на плоскость, касающуюся Земли в точке a (отмечу, что такая простая формула получается только для шара).

        Получаем вектор c (нужное направление) = — (b x a) x a / R3, где R — радиус Земли (тут я вычислил проекцию вектора на плоскость через двойное векторное умножение на нормаль к плоскости, после чего нормировал результат). В принципе, в эту формулу можно подставить формулы для векторов a и b — тогда R сократится. Только вот понятности в получившейся формуле будет ноль. Так что лучше считать последовательно, положив R=1.

        Теперь надо это направление перевести в угол. Для этого надо ввести вторую систему координат, местную. Ось аппликат в такой системе будет совпадать с вектором a, ось абсцисс будет касательной к параллели, а ось y — касательной к меридиану. Найдем ось абсцисс местной системы координат. Так как она — касательная к параллели, она должна лежать в ее плоскости, а нормаль этой плоскости равна k (т.е. оси аппликат геоцентрической системы). Кроме того, она должна лежать в плоскости, касающейся Земли в точке a. Прямая, которая лежит сразу в двух плоскостях, есть пересечение этих плоскостей. Ну а направляющий вектор такой прямой есть векторное произведение их нормалей.

        Мы нашли ось абсцисс местной системы координат — i1 = a x k / R. Ну, точнее, эта формула справедлива с точностью до знака — знак надо искать по «правилу буравчика» или любому другому применимому.

        Ось ординат j1 находится как произведение k1 x i1 = a1 x i1 / R.

        Теперь мы знаем проекции искомого направления на местные оси — (i1c) и (j1c), и угол найти довольно просто. В статье он уже был найден — но можно поступить еще проще и использовать стандартную библиотечную функцию «арктангенс двух аргументов» — в разных языках программирования она называется по разному, но суть у нее одна: вычисление угла поворота заданного вектора.

        И так, вот что получилось:
        a = ( cos ourLattitude * cos ourLongtitude, cos ourLattitude * sin ourLongtitude, sin ourLattitude )
        b = ( cos mekkaLattitude * cos mekkaLongtitude, cos mekkaLattitude * sin mekkaLongtitude, sin mekkaLattitude )
        c = - (b x a) x a
        i1 = (0, 0, 1) x a
        j1 = a x i1
        angle = arctan (i1 * c, j1 * c)
        

        Теперь осталось модифицировать angle с учетом heading. Разумеется, все эти формулы поддаются дальнейшему упрощению — только вот проводить его нет смысла.

        Разумеется, код я не проверял, да и ночь на дворе, поэтому он может содержать ошибки. Извиняюсь заранее.

        Полученного результата достаточно, чтобы примерно прикинуть направление. Ну а если нужна большая точность — то пора открывать специализированную литературу и не заниматься велосипедостроением.
          0
          точно, арктангенс от двух углов я для этого использовал, помню же что не арккосинус
            0
            Я вот думаю: а нужно ли рассчитывать путь по дуге окружности (кратчайшее расстояние на сфере)? Можно ведь найти направление прямого пути (сквозь Землю) на Мекку, и его проекция на плоскость, касательную к поверхности Земли, будет иметь то же направление, что и путь по дуге.
              0
              Я в итоге так и сделал, если вы не заметили.

              Но, кстати, ваш совет все равно очень ценный. Действительно, мусульман совсем не интересует кратчайшее расстояние, им нужно именно что направление. А это значительно упростит точные расчеты, с учетом настоящей формы Земли.
              +1
              Спасибо за объяснение. Теперь вижу, насколько наивным был мой подход.
                0
                главное что разобрались в итоге )
              +4
              О, а интересно, если Мекка ровно на противоположной стороне шарика, куда в таком случае нужно лицом садиться, и как?
                +1
                на противоположной стороне суши нет: «ru.m.wikipedia.org/wiki/%D0%A4%D0%B0%D0%B9%D0%BB:WorldMapWithAntipodes.png»

                мда, карму слили, ссылку парсер бьет
                  +1
                  Ну а что, на кораблях мусульмане не плавают?
                  +4
                  Ложиться лицом вниз, это просто. Гораздо сложнее ответить на вопрос куда смотреть жителям Мекки.
                    +2
                    Вообще-то на Каабу. Собственно все смотрят на Каабу, но для других стран точность такова, что проще смотреть на Мекку.
                • UFO just landed and posted this here
                    0
                    Если хочется серьезно работать с гироскопом в телефоне, то без базового понимания кватернионов делать там нечего. С другой стороны, не так уж это и сложно, разобраться вполне реально.
                      +2
                      Автор, не хочется Вас расстраивать (хотя выше уже расстроили), но Ваш компас указывает не на Мекку. Вы используете, по сути, меркарторовы координаты, а прямая в этих координатах (локсодрома) не является прямой (кратчайшим расстоянием, геодезической, ортодромой) на сфере.

                      Например, прямое (на сфере) направление на Мекку из Нью-Йорка выглядит так: yadi.sk/i/pKdLZT9IeMj49 (красная кривая). А синяя прямая — это, что получается по Вашему алгоритму. Сами видите, какая разница.
                        0
                        Все еще хуже: его координаты даже не меркаторовы.
                          0
                          Ну, масштаб по вертикали другой, но с точки зрения направлений это принципиально это картины не меняет.
                          0
                          Не расстроили, спасибо за объяснение.
                          0
                          Радуют переменные с названиями: angle1, angle2, angle3

                          Only users with full accounts can post comments. Log in, please.