Всем привет! Меня зовут Люба, и я инженер по тестированию в команде Ozon Partners Platform. Наша команда занимается поддержкой и развитием системы, предназначенной для развития сети пунктов выдачи Ozon. Одним из компонентов этой системы является Открытая карта.

Хочу рассказать, как мы научили автотесты находить на ней точки, подходящие для открытия нового ПВЗ.


Открытая карта — это интерактивный инструмент, на котором любой человек, может посмотреть, где можно открыть ПВЗ и сколько на этом заработать. На ней отмечены и уже открытые пункты выдачи, и рекомендованные зоны с финансовой поддержкой.

Рекомендованные зоны — это самые часто проходимые места, в которых ожидается наибольшая доходность. На карте они оформлены как вот такие красивые гексагоны.

Финансовая поддержка выплачивается первому агенту, открывшемуся в этой зоне. После того как в выделенном гексагоне ПВЗ переходит в подготовку к открытию, гексагон «гаснет». Если же по какой-то причине процесс открытия отменился, то гексагон «зажигается» снова.

В определённый момент перед нами встала задача «гасить» и соседние гексагоны. Кроме того, гексагоны должны «гаситься» и «зажигаться» по определённым правилам.

Как это выглядит:

Область с финансовой поддержкой до того, как оформлена заявка:
Область с финансовой поддержкой до того, как оформлена заявка:
После того, как заявка в центральном гексагоне получила финансовую поддержку:
После того, как заявка в центральном гексагоне получила финансовую поддержку:

То есть мы вырезаем гексагон, где открывается пункт, и все гексагоны вокруг него. Мы называем это «ромашкой».

Чтобы это реализовать, понадобилось находить соседние гексагоны. Трудность была в том, что у гексагонов не было никаких идентификаторов и связи с соседями.

Для решения этой задачи была выбрана библиотека H3 от Uber.

Наши аналитики давно используют эту библиотеку для анализа и визуализации результатов, где выгоднее всего было бы открыть пункт выдачи Ozon. Это была одна из причин выбора данного решения. 

Что это за библиотека?


H3 — это система геопространственного индексирования, разработанная компанией Uber. Она объединяет преимущества шестиугольной глобальной сетки с иерархической системой индексации.

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

Система H3 присваивает каждой ячейке уникальный иерархический индекс. Каждому направленному ребру и каждой вершине присваиваются индексы, основанные на исходной ячейке или ячейке-владельце, соответственно.

В библиотеке есть функция, которая находит соседние шестиугольники для заданного идентификатора H3 и возвращает список их идентификаторов. Таким образом, можно быстро найти ближайшие объекты, используя идентификаторы H3 и список соседних шестиугольников.

Так нам удалось решить задачу с поиском соседних гексагонов. 

Проблемы при подготовке тестовых данных для автотестов

Для автотестов нам нужны адреса, где будем оставлять заявки.

Распространённые библиотеки для генерации фейковых адресов нам не подходят по нескольким причинам:

  • у провайдеров геоданных, используемых на нашей карте, часто адреса, сгенерированные этими библиотеками, не находятся;

  • при выборе какой-либо страны, меняется и язык, на котором будет сгенерирован адрес. У нас же используется только русский язык, но уже есть несколько стран СНГ;

  • внутри самой карты есть много бизнес-логики, которую нужно учитывать при выборе адреса. Если при клике руками по карте это вcё видно, то автотесты так не умеют. Подробнее об этом ниже.

Под капотом наша Открытая карта похожа на слоёный торт.
Под капотом наша Открытая карта похожа на слоёный торт.

На карте существует множество слоёв, и каждый слой отвечает за свою бизнес-логику.

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

Обо всём этом рассказывается в онбординге при первом открытии карты:

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

В итоге мы остановились на следующем подходе.

Ищем у нас в списке заявок уже используемые адреса в нужной стране -> обрабатываем этот адрес, удаляем литеры и дроби, если есть -> к номеру дома добавляем рандомное число -> проверяем, есть ли данный адрес у наших провайдеров -> если есть, сохраняем, если нет — повторяем предыдущие шаги.

Далее адрес преобразуется в координаты. И вот на основе этих координат и начинаем готовить наши слои. Слои хранятся в формате GeoJSON.

GeoJSON — это специальный формат, предназначенный для хранения географических структур данных, основанный на JSON.

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

Когда была внедрена библиотека H3 для поиска соседних гексов, мы поняли, что можем использовать её и для автотестов. 

Как мы генерируем GeoJSON с помощью библиотек H3 для наших автотестов

Проект автотестов у нас написан на Python. В Python, как и во многих других языках, уже реализована данная библиотека.

С помощью встроенной функции geo_to_h3 реализуем функцию получения hex_id по координатам. В функцию передаём координаты lat долготу, lon широту и res разрешение.

Разрешение, грубо говоря, — это масштаб того, на насколько мелкие кусочки разбита вся сетка Земли и какого размера будет наш гексагон.

def get_hex_id_by_coordinates(lat, lon, res=9):
   with step('Получение hex_id по координатам'):
       hex_id = h3.geo_to_h3(lat, lon, res)
   return hex_id

Далее получаем координаты границ hex_id.

def hex_id_to_geo_boundary(hex_id):
   with step('Получение координат границ гексагона по hex_id'):
       geo_boundary = h3.h3_to_geo_boundary(hex_id, geo_json=True)
   return geo_boundary


В ответ получаем кортеж кортежей. Пример:

((104.2556598795973, 52.30893814231299), (104.25577105021553, 52.30709631208612), (104.25824446929087, 52.30622470385713), (104.26060678033717, 52.30719485928088), (104.26049581162792, 52.30903665544554), (104.25802232996648, 52.30990833025064), (104.2556598795973, 52.30893814231299))

Преобразуем ответ в список координат, чтобы получить координаты:

coordinates = [list(i) for i in geo_boundary]

Выходной результат сформированного GeoJSON:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Polygon",
        "coordinates": [
          [
            [
              104.2556598795973,
              52.30893814231299
            ],
            [
              104.25577105021553,
              52.30709631208612
            ],
            [
              104.25824446929087,
              52.30622470385713
            ],
            [
              104.26060678033717,
              52.30719485928088
            ],
            [
              104.26049581162792,
              52.30903665544554
            ],
            [
              104.25802232996648,
              52.30990833025064
            ],
            [
              104.2556598795973,
              52.30893814231299
            ]
          ]
        ]
      },
      "properties": {
        "fill": "#FFFF00",
        "fill-opacity": 0.55,
        "hex-id": "89257165053ffff"
          }
        }
      }
    }
  ]
}

В properties можно задать цвет, прозрачность, чтобы визуально на карте отличать тестовые гексагоны.

Всё это с помощью наших внутренних методов точечно загружаем на карту.

Так это выглядит на карте:

Чтобы сгенерировать гексагоны для «ромашки», используем метод библиотеки k_ring_distances:

def get_neighboring_hexs_id(hex_id, radius):
   """
    Функция формирует список соседних гексагонов от заданного hex_id в радиусе radius
    Для ромашки указать radius=1
    """
   neighboring_hexs_id = h3.k_ring_distances(hex_id, radius)
   neighboring_hexs_ids = list((neighboring_hexs_id[0]).union(neighboring_hexs_id[1]))
   return neighboring_hexs_ids

Здесь получаем список hex_id. И преобразуем в список координат.

def get_hexs_id_by_coordinates(list_coordinate, res):
   """
   Функция генерирует список hex_id по переданному списку координат.
   list_coordinate — список координат
   res — зум
   """
   with step('Сформировать список hex_id по переданному списку координат'):
       hex_id_list = []
       for coordinate in list_coordinate:
           lat = coordinate[0]
           lon = coordinate[1]
           hex_id = h3.geo_to_h3(lat, lon, res)
           hex_id_list.append(hex_id)
       return hex_id_list

Дальше аналогично формируем GeoJSON и загружаем на карту.
Таким образом, мы подготовили тестовые данные для наших тестов. И теперь мы можем генерировать любой слой из нашего «торта» и запускать любые тесты.




Далее перед нами стоит задача формировать области с финансовой поддержкой произвольной формы, и мы отказываемся от «ромашек», и библиотека H3 уже не будет использоваться. Однако в проекте автотестов останется формирование нужных нам слоёв в форме гексагонов с помощью библиотеки H3.

Комментарии (3)


  1. nail777
    13.08.2024 09:09

    А Место гарантированного дохода на стадионе (судя по карте) это тестовая штука или вы и пользователям такое предлагаете?


    1. strLubov Автор
      13.08.2024 09:09

      @nail777Данная картинка - это картинка с онбординга. Где описано, какой цвет, что означает на карте. Не является реальным местом.


      1. gazzz
        13.08.2024 09:09

        т.е. не шутка ?