Реализация класса географических координат: сравнение на равенство

Я интегрирую класс географических координат из CodePlex в мою личную библиотеку "инструментов". Этот класс использует float поля для хранения широты и долготы.

Так как класс GeoCoordinate инвентарь IEquatable<GeoCoordinate>Я обычно писал Equals метод вроде так:

public bool Equals(GeoCoordinate other)
{
    if (other == null) {
        return false;
    }

    return this.latitude == other.latitude && this.longitude == other.longitude;
}

На этом этапе я остановился и подумал, что я сравниваю переменные с плавающей запятой на равенство, что, как правило, нет-нет. Мой мыслительный процесс тогда шел примерно так:

  1. Я могу только представить установку Latitude а также Longitude свойства один раз, что означает, что не будет никаких ошибок, чтобы испортить мои сравнения.

  2. С другой стороны, можно (хотя и бессмысленно) писать

    var geo1 = new GeoCoordinate(1.2, 1.2);
    var geo2 = new GeoCoordinate(1.2, 1.2);
    
    // geo1.Equals(geo2) will definitely be true, BUT:
    
    geo2.Latitude *= 10;
    geo2.Latitude /= 10;
    
    // I would think that now all bets are off
    

    Конечно, это не то, что я могу себе представить, но если открытый интерфейс класса позволяет это, Equals должен быть в состоянии справиться с этим.

  3. Сравнение на равенство с использованием difference < epsilon test решит проблему сравнения двух экземпляров, но создаст больше проблем:

    • Как сделать равенство транзитивным? Это звучит невозможно.
    • Как создать одинаковый хеш-код для всех значений, которые будут сравниваться равными?

      Скажем так epsilon = 0.11 (случайный пример). Это следует из того GeoCoordinate { 1, 1 } потребуется тот же хэш-код, что и GeoCoordinate { 1.1, 1.1 }, Но последнему понадобится тот же хэш-код, что и GeoCoordinate { 1.2, 1.2 }, Вы можете видеть, куда это идет: все экземпляры должны иметь одинаковый хэш-код.

  4. Решением всего этого было бы сделать GeoCoordinate неизменный класс. Это также решило бы GetHashCode проблема: она основана на широте и долготе (что еще), и если они изменчивы, то с помощью GeoCoordinate в качестве ключа в словаре просят неприятности. Тем не менее, чтобы сделать класс неизменным, есть свои недостатки:

    • Вы не можете создавать и настраивать экземпляры класса (парадигма WPF), что в некоторых случаях может быть проблемой
    • Сериализация, вероятно, также стала бы проблемой из-за потери конструктора без параметров (я не эксперт по сериализации.NET, так что это так много деталей, как я вижу здесь)

Какой подход вы бы предложили? Легко заставить класс соответствовать требованиям, которые у меня есть сейчас (просто сделать его неизменным), но есть ли лучший способ?

Изменить: я добавил элемент 3 в списке выше, сдвинув предыдущий элемент 3 в положение 4.

Решение

Я собираюсь дать больше времени для обратной связи, но в настоящее время я иду с неизменным подходом. struct (потому что это то, что сейчас) с соответствующими участниками можно увидеть здесь; комментарии и предложения более чем приветствуются.

4 ответа

Решение

"Место" с долготой / широтой для меня довольно хорошо попадает в слот "неизменное значение". Сама позиция не меняется - если вы меняете широту, которая является другой позицией. Оттуда это может быть struct; за float struct в любом случае будет такого же размера, что и ссылка на x64, так что никакой реальной обратной стороны.

Ре равенство; если позиция не совсем такая же, она не "равняется", по крайней мере, в "ключевой" перспективе, поэтому я был бы счастлив == Вот. Вы можете добавить метод "находится в пределах (x) расстояния", если это помогло. Конечно, геометрия большой дуги тоже не совсем свободна;

Мысли хотя:

  • это должно переопределить bool Equals(object) а также добавление bool Equals(GeoCoordinate)
  • это должно переопределить GetHashCode() и реализовать IEquatable<GeoCoordinate>
  • статические операторы - это приятное дополнение
  • Equals в основном используется в словарях, поэтому правило, согласно которому вы должны сравнивать поплавки только с эпсилоном, здесь не применимо. Не пытайтесь поместить эпсилон логику в Equals, Это работа пользователей делать это так, как ему нужно. Относительно легко доказать, что единственная хеш-функция, которая соответствует эпсилон-сравнениям, это постоянная хеш-функция.

  • Ваша реализация имеет одну проблему: она не обрабатывает NaNs. Вам нужно использовать Equals вместо == по индивидуальным координатам для вашего Equals метод.

    public bool Equals(GeoCoordinate other)
    {
        if (other == null) {
            return false;
        }
    
        return this.latitude.Equals( other.latitude) && this.longitude.Equals(other.longitude);
    }
    
  • "Не сравнивайте, используя == но использование epsilon "указание предназначено для потребляющего кода, а не для реализующего кода. Поэтому я бы реализовал функцию, которая возвращает расстояние между двумя гео-координатами и велел бы пользователю использовать это для своих сравнений epsilon.

  • Я бы определенно сделал его неизменным (не уверен, что это структура или класс). Он имеет семантику значений и поэтому должен быть неизменным.
    Я обычно использую что-то вроде Linq-to-Xml/Linq-to-Json для сериализации, поскольку это позволяет мне преобразовать представление между моей моделью в памяти и моделью на диске.
    Но вы правы, что многие сериализаторы не поддерживают конструкторы не по умолчанию. Я считаю это большим недостатком в этих сериализаторах, а не недостатком в моей модели. Некоторые сериализаторы просто получают доступ к закрытым сеттерам / полям, но лично я думаю, что воняет.

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

В зависимости от того, как вы используете структуру lat/lon, я бы хотел использовать float вместо double для lat/lon. Например, если вы выполняете интеграцию широты / долготы в реальном времени, вам потребуется двойная точность. Это связано с тем, что степень составляет 1 морскую милю, и в зависимости от временного шага интеграции вы перемещаете очень маленькое количество за итерацию времени.

Для выполнения теста на равенство я использовал бы простую формулу расстояния, основанную на равностороннем приближении, и решил, что если бы другая точка находилась в пределах моего установленного допуска (дюйм или два), то объявил бы их равными. Равноугольное приближение, которое я использовал бы, дано здесь.

Другие вопросы по тегам