Skip to content

aeuge/mongodb_geo-1

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Аспекты учета и поиска геоинформационных объектов с задействованием возможностей документоориентированной базы данных MongoDB.

Онлайн-курс: "OTUS.NoSQL".

Период обучения: 30 сентября 2020 года — 4 апреля 2021 года.

«Лучше один раз увидеть, чем сто раз услышать!» (пословица)

Введение

Геоинформация - это любые сведения, отражающие расположение, форму и размеры объекта (далее - гео-объект). Ее учет ведется в картографии, геологии, метеорологии, землеустройстве, экологии, муниципальном управлении, транспорте, экономике, обороне и многих других областях. Геоинформация является неотъемлемой частью так называемых Больших данных, что приводит к необходимости разработки средств ее анализа и визуализации.

Академический подход написания статей подразумевал "сведения, отражающие свойства ... объектов материального мира". Однако на практике имелся факт осуществления энтузиастом накладки поверх Гугл-карт через штатное API рисунков с топографией Средиземья и построение маршрутов героев Дж. Толкина, что не совсем "материально". Другим стыком с нематериальным может служить пример наборов данных по типу GeoIP, E.164, ABC.

Результат исследования представляет собой инструмент отображения хранящихся в MongoDB сведений о гео-объектах на карте посредством web-доступа. Клиентская часть реализована с использованием Leaflet (JavaScript-библиотека с открытым исходным кодом для мобильных интерактивных карт) и набора соответствующих процедур асинхронного получения сведений от серверной части. Сервис разработан на базе созданного ранее на курсе "OTUS.Web-python" конструктора программного обеспечения "Dummy WSGI Framework" (репозиторий) на языке программирования Python3 с задействованием WSGI.

В настоящем тексте основное внимание уделено простоте работы с геоинформацией средствами MongoDB. Особенности реализации процедур сбора демонстрационных сведений отражены по мере изложения.

Необходимо заметить, что документация MongoDB является достаточно проработанной и удобной по навигации. Исчерпывающие сведения в разделе по гео-объектам подтолкнули к изучению изложенных в данной работе возможностей. Кроме того, после осуществленной разработки и достигнутых результатов в ходе написания настоящего текста в документации MongoDB обнаружен пример работы с гео-объектами, что еще раз подтверждает ее высокую проработку.

Гео-объекты MongoDB

В данной статье продемонстрированы базовые возможности оперирования в MongoDB такими гео-объектами как: точка, линия, полигон.

Более сложные структуры, такие как: набор точек, набор линий, набор полигонов и коллекция гео-объектов не рассматриваются.

Представляемое в рамках настоящей работы решение предполагает возможность хранения в обособленных коллекциях одной базы данных сведений о гео-объектах различных типов. Тем не менее хранение также возможно в рамках одной коллекции. Названия полей, описывающих гео-объекты, могут быть произвольными, за исключением уникально идентифицирующего атрибута документа - ident. Также необходимо соблюдать установленную MongoDB структуру сведений о гео-объекте:

<field>: { type: <GeoJSON type> , coordinates: <coordinates> }

В рамках получения сведений клиентской стороной от серверной гео-объекты искусственно поделены на два вида - статичные и динамичные.

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

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

Предполагается, что инфраструктура MongoDB развернута.

mongo  192.168.102.99  --port 49326
---
> use otus
switched to db otus
> db.dropDatabase()
{ "dropped" : "otus", "ok" : 1 }
> use otus
switched to db otus
> db
otus
> show collections

Точки (статичные)

В качестве точек используются метеориты. Необходимые коллекция, поля и индексы:

> db.meteorites.createIndex( { "ident": 1 }, { unique: true } )
> db.meteorites.createIndex( { "location" : "2dsphere" } ) 

Изначально сведения о местоположении хранятся в атрибуте geolocation. Однако оно не имеет необходимой структуры для гео-объекта типа "точка". Поэтому в качестве атрибута местоположения метеоритов выступает дополнительное, отсутствующее в демонстрационном наборе, поле location, куда перенесены сведения в необходимом формате:

location: { type: 'Point' , coordinates: [ LON, LAT ] }

Загрузка исходных данных о метеоритах:

mongoimport --host 192.168.102.99  --port 49326 \
--db otus --collection meteorites --jsonArray \
--file ./foreign/meteorites/data.json

    2021-03-28T10:28:09.443+0300    connected to: mongodb://192.168.102.99:49326/
    2021-03-28T10:28:12.443+0300    [###.....................] otus.meteorites      1.62MB/10.1MB (16.0%)
    2021-03-28T10:28:15.443+0300    [#########...............] otus.meteorites      3.97MB/10.1MB (39.4%)
    2021-03-28T10:28:18.443+0300    [############............] otus.meteorites      5.39MB/10.1MB (53.4%)
    2021-03-28T10:28:21.443+0300    [#################.......] otus.meteorites      7.23MB/10.1MB (71.6%)
    2021-03-28T10:28:24.443+0300    [#####################...] otus.meteorites      8.83MB/10.1MB (87.5%)
    2021-03-28T10:28:27.443+0300    [#######################.] otus.meteorites      9.71MB/10.1MB (96.3%)
    2021-03-28T10:28:28.453+0300    [########################] otus.meteorites      10.1MB/10.1MB (100.0%)
    2021-03-28T10:28:28.454+0300    45716 document(s) imported successfully. 0 document(s) failed to import.

Исходя из уже имеющегося опыта, из набора 45716 объектов необходимо удалить метеорит, который не относится к земной поверхности (марсианский метеорит Meridiani Planum), так как его координаты не соответствуют стандарту земного геопозиционирования и не могут быть помещены в индекс (индексирование приводит к ошибке Can't extract geo keys: ... longitude/latitude is out of bounds, ..., равно как и вставка таких данных в индекс).

db.meteorites.remove({"ident" : "32789"});

Кроме этого в наборе имеется 7315 метеоритов, чье местоположение не известно. Данный факт также не позволит включить их в гео-индекс и приведет к ошибке. Поэтому в индексе учтены только те метеориты, чье местоположение известно.

db.meteorites.updateMany( 
    {"geolocation":{$exists:true}},
    [{
        $set: {
            "location" : {
                "type": "Point",
                "coordinates" : [ 
                    { $toDouble: "$geolocation.longitude" } , 
                    { $toDouble: "$geolocation.latitude" } 
                ]
            }
        }
    }]
);

В результате в MongoDB в коллекции meteorites в атрибутах location содержится информация о местоположении 38400 метеоритах из их общего числа 45716.

Важное замечание: согласно документации данный порядок следования координат { долгота, широта } является единственно верным с точки зрения MongoDB (If specifying latitude and longitude coordinates, list the longitude first and then latitude). Необходимо заострить внимание на этом обстоятельстве, так как в последующем при отображении информации на карте Leaflet нужен будет другой порядок абсолютно для всех координат любых гео-объектов - { широта, долгота }. Указанное приводит к тому, что после получения сведений из MongoDB требуется произвести перестановку в парах координат или представить их в виде "словаря" { lon: долгота, lat: широта }. Если для точки это выражается в одной перестановке, то для полигона это происходит в рамках итерации по точкам границы. Было бы хорошо, если бы MongoDB поддерживала хранение и в формате { широта, долгота }.

Полигоны (статичные)

В качестве полигонов используются здания и сооружения, сведения о которых единоразово получены для отдельного района г. Санкт-Петербурга с использованием API сервиса WikiMapia.

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

Необходимые коллекция, поля и индексы:

db.geo_wikimapia_polygons.createIndex( { "ident": 1 }, { unique: true } )
db.geo_wikimapia_polygons.createIndex( { "area" : "2dsphere" } ) 

Сбор демонстрационных данных реализован на языке программирования Python3 с использованием библиотеки pymongo.

Вниманию Python-разработчиков: в исходном коде данной процедуры исключено исполнение набора инструкций вставки-обновления (UPSERT) за раз (bulk_write(instructions)), так как при наличии ошибки (о чем сказано ниже) соответственно он отвергался полностью. Вставка-обновление происходит последовательно по одной инструкции.

Изначально сведения о местоположении полигона разнесены по полям записи, то есть фактически отсутствует атрибут необходимой для MongoDB структуры для гео-объекта типа "полигон". Поэтому в его качестве выступает дополнительное поле area, куда перенесены сведения в необходимом формате (происходит сразу при вставке сведений в рамках их онлайн-получения). Структура гео-типа полигон имеет вид:

area: { type: 'Polygon' , coordinates: [[ [LON_1, LAT_1], [LON_2, LAT_2], ..., [LON_1, LAT_1] ]] }

В результате запросов к WikiMapia:

python3 ./foreign/onetime_static_load_polygons_wikimapia.py 

    Page 1 has docs count 50
    Page 2 has docs count 50
    ...
    Page 37 has docs count 35
    Max page 37 with some data

в MongoDB накоплена информация в отношении:

> db.geo_wikimapia_polygons.count()

1832 зданий и сооружений.

Важное замечание: в MongoDB сведения о полигоне должны удовлетворять спецификации (пункт 3.1.6 RFC 7946 "GeoJSON" August 2016). В частности, полигон, имеющий пересечение граней, не может быть добавлен (иначе в MongoDB возникает ошибка Edges <number K> and <number M> cross. Edge locations in degrees: [Kx1, Ky1]-[Kx2, Ky2] and [Mx1, My1]-[Mx2,My2]). Кроме того, важно, чтобы полигон был "замкнут", то есть крайняя точка должна совпадать и первоначальной (иначе в MongoDB возникает ошибка Loop is not closed). WikiMapia же иным образом подходит к требованиям достоверности координат. Поэтому из 1835 полигонов, полученных в описанном ранее абзаце (36 страниц * 50 полигонов + 35 полигонов = 1835 полигонов), сохранены 1832 объекта.

Линии (динамичные)

В качестве демонстрационных динамичных гео-объектов выбраны линии и точки, отражающие крайние части маршрутов и фактическое пребывание автомобилей таксопарка компании "Яндекс" соответственно.

Важное замечание: сведения о линии должны удовлетворять спецификации (пункт 3.1.4 RFC 7946 "GeoJSON" August 2016), то есть линия должна содержать две разные точки (при этом она может также содержать и одинаковые).

Сбор демонстрационных данных реализован на языке программирования Python3 с использованием библиотек requests, pymongo и задействованием пакета многопроцессорной обработки multiprocessing. Необходимость крайнего обусловлена требованиями увеличения скорости получения актуальных сведений о местоположении и пути следования автомобилей с целью повышения эффективности частоты прорисовки маршрутов на карте (максимизации интерактивности). Сведения получаются в отношении заранее определенных точек района г. Санкт-Петербурга. Точки сбора данной информации располагаются на определенном коротком расстоянии друг от друга и образуют заранее рассчитанную в проекте "ячеистую" структуру. Данный подход отличается от алгоритма "заливки", применявшегося иным разработчиком, исследовавшим подобную информацию ранее.

Вниманию Python-разработчиков: невозможно организовать пул процессов, к которым применены декораторы. Необходимо переписать код таких функций с условием внесения в их содержание "оберточной" логики.

Изначально сведения о маршруте не имеют необходимой MongoDB структуры для гео-объекта типа "линия". Поэтому в качестве данного атрибута выступает дополнительное поле path, куда перенесены сведения в необходимом формате. Структура гео-типа линия имеет вид:

path: { type: 'LineString' , coordinates: [ [LON_1, LAT_1], [LON_2, LAT_2], ..., [LON_N, LAT_N] ] }

Операции с гео-объектами

В рамках работы рассматриваются такие операции как: $geoIntersects и $nearSphere.

Операция $geoIntersects является основной реализованной в проекте и используется для нахождения всех гео-объектов, имеющих пересечение по гео-расположению с текущей областью карты. Например, если в область карты (соответствующий полигон, описываемый двумя крайними диаметрально расположенными координатами) попадет часть маршрута (линии) или часть здания (полигона), то они будут получены из базы данных и отображены на карте. Точки, соответственно, появятся при их попадании в данную область.

Важное замечание: при строгом подходе и оценке данного исследования можно утверждать, что оно фактически основано на единственном запросе "найти все объекты в области на карте". Однако, в рамках защиты необходимо заметить, что конечной целью являлась демонстрация именно простоты работы с геоинформацией в MongoDB.

Использование иной операции $nearSphere продемонстрировано на примере выборки из MongoDb полигонов и метеоритов с целью отображения их на карте только при условии присутствия их в "круговой" окрестности наблюдения (для полигона - пересечение с окружностью).

Операции $geoWithin (выборка гео-объектов, имеющих полное включение в заданную область) и $near (выборка гео-объектов в окрестности точки) в рамках настоящей работы не рассматриваются.

Операции $near и $nearSphere шире по возможности, чем просто "нахождение в круговой окрестности", так как описывают не только максимальное удаление ($maxDistance) от точки наблюдения, но и минимальное ($minDistance). Данное обстоятельство может быть использовано при работе с секторами, учитывающими "примерное" удаление объектов от исходной точки наблюдения: секторное поле зрения на панораме "Яндекс.Карты", "классические" области действия сотовых вышек, углы обзора видеокамер городского наружного наблюдения и иное.

Сервис демонстрации гео-объектов

Сервис реализован на базе авторского конструктора программного Web-обеспечения "Dummy WSGI Framework" (репозиторий), является легковесным (45 килобайт кода).

pip3 install -r ./service/requirements.txt
uwsgi --http 127.0.0.1:8080 --wsgi-file ./service/application.py

Обстановка в области карты

По адресу http://127.0.0.1:8080 вниманию представлен район г. Санкт-Петербурга, где отображены полигоны зданий. По мере перемещения по карте или изменении масштаба карты в асинхронном режиме подгружаются иные полигоны.

Разработчикам на заметку: для записи GIF-анимации использовалось программное обеспечение Peek.

POLYGONS

При перемещении в определенные районы (достаточно в исходной точке г. Санкт-Петербурга уменьшить масштаб до размера "7") подгружаются указатели на конкретные места падения метеоритов.

METEORITES

С целью демонстрации в текущем приложении нет ограничения на степень масштабирования, при которой запросы к серверной части не происходят. Тем самым представляется возможным увидеть скопления метеоритов на карте достаточно малого масштаба. Однако в качестве рекомендаций при работе с данными, имеющими значительный "мировой" объем, можно посоветовать использование имеющегося в Leaflet параметра минимально допустимого уровня масштабирования карты.

Очередным открытием в данных NASA явилось наличие 64 метеоритов, местоположения падения которых имеют абсолютно одинаковые координаты. Указанное обнаружено в результате визуального изучения метеоритов на карте (выделяющаяся темная тень).

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

METEORITES

> db.meteorites.find({"location.coordinates": [13.43333,58.58333] }).count()
    64
> db.meteorites.find({"location.coordinates": [13.43333,58.58333] }, {name: 1, _id: 0})
    { "name" : "Osterplana" }
    { "name" : "Österplana 002" }
    { "name" : "Österplana 003" }
    ...
    { "name" : "Österplana 064" }

Данные "необычные" сведения соответствуют метеориту "Österplana", имеющему удивительную историю (рус.).

Для интерактивной демонстрации динамичных гео-объектов необходима следующая коллекция:

db.geo_yandex_taxi.deleteMany({})
db.geo_yandex_taxi.createIndex( { "ident": 1 }, { unique: true } )
db.geo_yandex_taxi.createIndex( { "last_point" : "2dsphere" } )
db.geo_yandex_taxi.createIndex( { "path" : "2dsphere" } )

и фоновый сбор актуальных данных:

python3 ./foreign/upsert_yandex_taxi_loop.py 

    9       2.6140940189361572
    9       2.481816291809082
    9       2.528238296508789
    9       2.374605894088745
    9       2.5337154865264893
    9       2.7297616004943848
    9       2.60577392578125
    9       2.586944818496704
    9       2.5660433769226074

Исходные сведения о перемещении таксопарка "Яндекс" обезличены и содержат крайние точки пройденных маршрутов:

{'id': 'bcc095db8e3b56e057caebdb97af5694', 'display_tariff': 'business', 'free': True, 'static_icon': False, 
'positions': [{'lon': 30.326291, 'lat': 59.974395, 'direction': 50.0, 'timestamp': '2021-03-24T23:49:01.000000+0000'}, 
{'lon': 30.326291, 'lat': 59.974395, 'direction': 50.0, 'timestamp': '2021-03-24T23:48:52.000000+0000'}, 
{'lon': 30.326291, 'lat': 59.974395, 'direction': 50.0, 'timestamp': '2021-03-24T23:48:43.000000+0000'}, 
{'lon': 30.326291, 'lat': 59.974395, 'direction': 50.0, 'timestamp': '2021-03-24T23:48:34.000000+0000'}]}

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

На их основании сформированы линия перемещения и точка крайнего местоположения.

TAXI

Помимо мониторинга на предмет появления нового динамичного объекта в текущей области карты системой осуществляется проверка покидания объектами ее границ. В ином бы случае движущиеся объекты останавливались и скапливались по краям карты.

TAXI

Обстановка в области круговой окрестности

По адресу http://localhost:8080/circle/ продемонстрирована выборка только тех гео-объектов, которые попадают в круговую окрестность, располагаемую по центру карты.

NEAR

Важное замечание: с целью использования near и nearSphere требуется создать индекс 2d или 2dsphere, иначе их вызов приведет к ошибке исполнения:

error processing query: ns=otus.geo_wikimapia_polygonsTree: GEONEAR  field=area maxdist=500 isNearSphere=0
Sort: {}
Proj: { _id: 0 }
 planner returned error :: caused by :: unable to find index for $geoNear query, full error: {'ok': 0.0, 'errmsg': 'error processing query: ns=otus.geo_wikimapia_polygonsTree: GEONEAR  field=area maxdist=500 isNearSphere=0\nSort: {}\nProj: { _id: 0 }\n planner returned error :: caused by :: unable to find index for $geoNear query', 'code': 291, 'codeName': 'NoQueryExecutionPlans'}

Технические особенности реализации

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

Конфигурационный файл

Параметр конфигурации сервиса base_config.py содержит отсылку на вид ("статичный", "динамичный"), название коллекции базы MongoDB ("meteorites", "geo_wikimapia_polygons", "geo_yandex_taxi") и атрибуты ("location", "area", "last_point", "path"), содержащие сведения о гео-объектах с указанием их GeoJSON-типа ("Point", "LineString", "Polygon"), а именно:

...
MONGODB_DB_COLLECTIONS = dict(
    static={
        "meteorites": {
            "location": POINT_OBJECT,
        },
        "geo_wikimapia_polygons": {
            "area": POLYGON_OBJECT,
        },
    },
    dynamic={
        "geo_yandex_taxi": {
            "last_point": POINT_OBJECT,
            "path": LINE_STRING_OBJECT,
        },
    },
)
...

Таким образом, при необходимости отображения на карте сведений из иной коллекции необходимо определиться с их видом ("статичный", "динамичный"), типом ("точка", "линия", "полигон") и названием конкретного атрибута, где они сохранены в требуемой MongoDB структуре.

При отсутствии в конфигурации динамичной коллекции, генерируемая сервисом HTML-страница не содержит инструкцию на JavaScript, осуществляющую периодический запрос сведений при неизменной локации. Указанное устраняет излишнюю нагрузку на серверную часть сервиса.

Изменения конфигурации вступают в силу после перезапуска сервиса, работа исключительно с HTML-файлами данных действий не требует.

"Ненагружающий" запоздалый AJAX запрос

Получение сведений клиентской частью от серверной реализовано с использованием асинхронных запросов на JavaScript (AJAX). При этом с целью недопущения исполнения сервисом запросов, являющихся "промежуточными", запросы происходят с некоторой малой задержкой, во время которой данный запрос может быть отменен последующим обращением. То есть результативная выборка данных происходит только в конечном положении наблюдения. Данный подход снижает нагрузку на серверную часть при последовательном переходе от участка карты к участку или при работе с ее масштабированием за счет исключения к ней AJAX-запросов, фактическое исполнение которых не требуется.

 function get_data(...){
    ...
    if (xhr && !(xhr.readyState === 4)) {
        xhr.abort();
        console.log('Previous AJAX #' + xhr.__dt + ' was aborted');
    }
    clearTimeout(timer);
    xhr = new XMLHttpRequest();
    xhr.responseType = 'json';
    xhr.__dt = Date.now();
    console.log('Start AJAX #' + xhr.__dt);
    timer = setTimeout(function() {
        // find objects in area.
    }
}

Работа по старту AJAX-запроса и его прерыванию, если браузер осуществил новое обращение, продемонстрирована ниже.

CLEVER_AJAX

Направления работы

Соответствие координат

MongoDB использует систему координат WGS84 (MongoDB geospatial queries on GeoJSON objects calculate on a sphere; MongoDB uses the WGS84 reference system for geospatial queries on GeoJSON objects) (поиск в глоссарии слова "wgs84").

При этом Leaflet по-умолчанию использует систему координат EPSG 3857.

Исходя из описания, EPSG 3857 допустима для координат между 85.06°S и 85.06°N.

То есть в рамках рабочей эксплуатации необходимо в Leaflet установить параметр CRS равным "L.CRS.EPSG4326", поскольку он не имеет таких ограничений и целиком соответствует системе геокодирования MongoDB.

Запредельные координаты

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

pymongo.errors.OperationFailure: longitude/latitude is out of bounds, lng: 561.213 lat: 89.9823 ... 

Valid longitude values are between -180 and 180, both inclusive.
Valid latitude values are between -90 and 90, both inclusive.

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

β-тестирование

В рамках работы не рассматривались вопросы шардирования сведений о гео-объектах (операции их поддерживают в MongoDB у версий выше 4.0) и не исследовалась отдача при нагрузке.

Расширение числа источников

В результате рефакторинга представляется возможной организация получения сведений не только из разных коллекций, как это реализовано в настоящее время, но и от разных баз данных MongoDB. Указанное значительно повысит гибкость использования разработанного инструмента.

Выводы

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

Вместо заключения

Лучше один раз увидеть, чем сто раз услышать.

8

9

Спасибо курсам "OTUS" за приобретенный опыт Fullstack-разработки (в частности интеграции Python, Javascript и MongoDB).

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Python 59.5%
  • HTML 40.5%