Идея спасти мир и при этом заработать немного шекелей витала у меня в голове уже давно. Имея неплохой накопленный опыт в области геоинформационных систем и защитивши в свое время диссертацию с их применением мне не хватало знаний разработчика. Окончив IT-курсы и получив доступ к «Святому Граалю знаний» я понял, – пора, и завертелось!

Летом 2024 года мы в составе команды «Arrow» одержали победу, заняв третье место в хакатоне «Лидеры цифровой трансформации» и вошли с нашим проектом в топ-100, став резидентами «Академии инноваторов» у нас появился свой стартапп.

Общая идея такова. Arrow - это платформа для анализа и обработки спутниковых снимков, использующая технологии машинного обучения и нейросетей для мониторинга окружающей среды, строительства и природопользования. Наш продукт помогает бизнесу и государственным структурам автоматизировать выявление экологических нарушений и незаконных построек, обеспечивая более точное и своевременное реагирование. Это в «розовом» будущем, а пока это только проект «Мобильное приложение для управления антропогенной нагрузкой на особо охраняемых природных территориях Камчатского края», занявшее призовое место, хотя и этот результат тоже когда-то был только в мечтах.

Я хочу открыть целый цикл статей в котором постараюсь осветить историю жизненного цикла нашего проекта «Arrow», которая будет писаться на ваших глазах. Здесь будет все: и фронт и бэк и мобильная разработка, будет и деплой в облако. В этих статьях, которые к стати буду писать не только я, но и ребята с моей команды, мы хотим осветить все начиная от создания MVP (минимально жизнеспособный продукт) и заканчивая выводом проекта в продакшн, анализ целевой аудитории и поиск первых клиентов, привлечение первых инвестиций, подбор команды, в общем все этапы через которые нам предстоит пройти для достижения своей цели, - получения интересного и востребованного продукта. Начнем же…

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

Данная статья освещает практический подход для решения задачи построения Веб-ГИС приложения и сервисов на основе открытых ресурсов и на примере нашего проекта. Основное внимание в ней будет уделено созданию общей структуры проекта и освещению ресурсов на которых он функционирует.

Проект реализован с применением клиент-серверной архитектуры (рисунок 1). Серверная часть включает в себя СУБД для хранения векторных и растровых наборов данных, файловое хранилище растровых тайлов, сервер приложений Gunicorn, обслуживающий сам проект, и веб-сервер Nginx для взаимодействия, через протокол REST full с сервисами приложения, программ клиентов, которыми могут быть десктопные и (или) мобильные и (или) веб-браузеры. Таким образом, весь проект будет размещен в облаке Reg.ru о чем в дальнейшем будет отдельная статья, а пока мы работаем на нашей локальной машине (localhost), с организацией туннелированного доступа с применением Ngrok.

Рисунок – 1. Архитектура проекта
Рисунок – 1. Архитектура проекта

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

  • СУБД Postgresql с надстройкой PostGIS, которая предоставляет дополнительные возможности для работы с геопространственными данными, превращая базу данных в геореляционную;

  • Широко распространенный скриптовый язык программирования Python;

  • Не менее широко известный фреймворк, предназначенный для создания веб-приложений Django, взаимодействующий с СУБД с помощью технологии ORM (Object Relations Mapping). Эта технология позволяет работать с выбранной СУБД посредствам классов языка программирования, меняя лишь специальный драйвер, минуя при этом необходимость в написании SQL-запросов и, что самое главное, без изменения исходного программного кода приложения;

  •  Для взаимодействия клиента с серверной частью предназначен Django REST фреймворк. DRF – одна из современных технологий создания API (программного интерфейса приложения), основным преимуществом которой является возможность организации совместной работы клиентских и серверных программ, написанных на различных языках программирования;

  • Библиотека GDAL содержит в себе полноценный функционал современных ГИС (геоинформационных систем) общего назначения, объединяя в себе две библиотеки – одноименную gdal, для работы с растровыми наборами данных и ogr, для работы с векторными форматами данных (shape, kml, geojson и др.). Активация этой библиотеки превращает фреймворк Django в GeoDjango;

  • Для анализа спутниковых снимков (получаемых через API  Sentinel Hub) применяются библиотеки Numpy и Pandas, дополнительно используется Jupiter Notebook, в том числе для работы со сверточной нейронной сетью      U-Net для сегментации изображений; 

  • Библиотека Leaflet (либо OpenLayer 3) посредствам java-скриптов и во взаимодействии с html-шаблонами и таблицами стилей CSS, визуализирует геоданные в окне веб-браузера и предоставляет пользователю элементы интерфейса для работы с серверным приложением;

  • В качестве карты-подложки применяется OSM-карта.

Создание проекта предполагает установку языка программирования, модуля для работы с виртуальными средами venv, пакетного менеджера pip, СУБД Postgrsql с расширением PostGIS, в качестве редактора кода я использую VSCode. В зависимости от операционной системы для установки всего этого добра используются различные менеджеры, например в MacOS это brew, в Ubuntu – apt, в Mandjaro pacman и т.д. Я работаю еще в Windows, но там использую стандартные .exe-шники. Тут и далее буду работать с Ubuntu, так как в дальнейшем при деплое на удаленном сервере Reg.ru тоже будет применяться этот дистрибутив Linux.

sudo apt install python3-venv python3-pip postgresql postgis

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

sudo su postgres
psql
ALTER USER postgres WITH PASSWORD `admin`;

Теперь создаем базу данных.

CREATE DATABASE test;

Далее нужно активировать для этой базы данных расширение postgis. Подключаемся к базе.

\c test

И активируем расширение.

CREATE EXTENSION postgis;

Со списком всех доступных расширений для работы с пространственными данными и для их активации рекомендую ознакомится со справкой на сайте PostGIS.

Выходим.

\q

Переключаемся с пользователя postgres на пользователя операционной системы.

exit

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

Следующим шагом нам необходимо создать виртуальное окружение для размещения в нем нашего проекта и установить в него необходимые зависимости, главные из которых фреймворки django и djangorest, а также драйвер для работы с базой данных Postgresql – psycopg2-binary.

Создаем папку для размещения проекта.

mkdir geodjango

Переключаемся на эту папку, это будет наша рабочая директория.

cd geodjango 

Создаем окружение, где env – это названия окружения.

python –m venv env

И активируем его (для выхода из виртуального окружения необходимо запустить файл deactivate).

source env/bin/activate

Теперь установим в виртуальное окружение необходимые для работы проекта зависимости. Список этих зависимостей содержится в файле requirements.txt.

pip install –r requirements.txt

Можно проверить все ли необходимые зависимости установлены командой pip freeze (с их версиями). 

Находясь в рабочей директории «geodjango», приступаем к созданию проекта и приложения в нем.

Создание проекта выполняется командой, где geodjango – это название проекта.

django-admin startproject geodjango

Создание приложения выполняется командой, где geoapp  – это названия приложения.

python manage.py startapp geoapp

При создании проекта и приложения в нем с помощью фреймворка Django формируется первоначальная файловая структура. Каждый из этих файлов имеет свое строгое предназначение. Например, файл settings.py, в том числе, предназначен для подключения к предварительно созданной на основе шаблона PostGIS базе данных.

Для настройки подключения используется секция DATABASES.

DATABASES = {
    'default': {
        'ENGINE': 'django.contrib.gis.db.backends.postgis',
        'NAME': 'test',
        'USER': 'postgres',
        'PASSWORD': 'admin',
        'HOST': 'localhost',
        'PORT': '5432',
    }
}

Кроме того в этом файле нужно зарегистрировать вновь созданное приложение. Это делается в секции INSTALLED_APPS.

INSTALLED_APPS = [
    ...
    'django.contrib.gis', # Настраеваем geodjango
    'rest_framework', # REST API
    'geoapp', # Новое приложение
    'corsheaders', # CORS
]

В этой же секции включаем GeoDjango, DRF (Django REST Framework) и CORS (Cross-Origin Resource Sharing).

По умолчанию Django не допускает запросы кросс-домены. CORS (Cross-Origin Resource Sharing) – это механизм веб-безопасности, открывающий доступ к серверу скриптам веб-страниц из другого домена. Если не настроить эту защиту консоль браузера будет выдавать сообщение об ошибке CORS.

В Django вы можете использовать пакет django-cors-headers для управления настройками CORS.

pip install django-cors-headers

Обратите внимание: обязательно нужно добавить 'corsheaders.middleware.CorsMiddleware' в секцию MIDDLEWARE перед CommonMiddlewareи настроить секцию CORS_ALLOWED_ORIGINS, перечислив те домены, доступ с которых к вашему API, для осуществления кросс-доменных запросов, должен быть разрешён. Теперь ваш API способен работать с CORS.

MIDDLEWARE = [..., 'corsheaders.middleware.CorsMiddleware', ...]
CORS_ALLOWED_ORIGINS = ["https://example.com", "http://localhost:3000"]

Django REST Framework - это мощный набор инструментов для создания веб-сервисов и API. Этот фреймворк тоже нужно дополнительно установить и не забыть прописать в секцию INSTALLED_APPS.

pip install djangorestframework

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

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

class ImportTrek(models.Model):

    class Meta:
        verbose_name_plural = 'Импорт точек маршрута'
        db_table = "trek_model" # название модели в БД
    name = models.CharField(max_length=250, default=' - ', blank=False, verbose_name='Название точек')
    # Это поле хранит пары координат точки    
    location = models.PointField(srid=4326, verbose_name='Местонахождение точки')
    
    # Переопределим название экземпляра модели в административной панели.
    def __str__(self):
        return f'{self.name}'

В этом же файле между моделями создаются реляционные связи различных типов кардинальности (1:1, 1:M, N:M). В примере ниже создана связь типа много-ко-многим между двумя моделями ImportTrek и ImportTrekLine .

class PointInLine(models.Model):
    class Meta:
        verbose_name_plural = 'Таблица M:N точки-линии'
        db_table = "relations_p_l_model" # название модели в БД

    mypoints = models.ForeignKey(ImportTrek, on_delete=models.CASCADE, related_name='mylines') 
    mylines = models.ForeignKey(ImportTrekLine, on_delete=models.CASCADE, related_name='mypoints') 

Обратите внимание на параметр related_name, создаваемых внешних ключей mypoints и mylines, они используются для создания более осмысленных и удобных имен для обратной связи между моделями в замен на создаваемые Django по умолчанию.

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

# Регистрируем модель точек в админпанели GeoDjango
@admin.register(ImportTrek)
class ImportTrekAdmin(admin.GISModelAdmin):
    # начиная с django v.4 использовать GISModelAdmin, ниже - OSMGeoAdmin
    list_display = ('name', 'location', ) # Поля модели, которые мы хотим видеть в панеле администратора

Каждое внесение изменений в модели базы данных требует создание миграций.

python manage.py makemigrations

Команда ниже создает модели в базе данных.

python manage.py migrate

Для взаимодействия с данными базы предназначен файл views.py. Он содержит функции и классы представлений, описывающих программную логику приложения. Например ниже представлена функция, которая добавляет созданные маркеры (точки) в базу данных.

# сервис добавления событий в базу данных    
@api_view(['POST'])
def create_point(request): 
    '''
    import requests
    url = "http://127.0.0.1:8000/api/create_point/"
    response = requests.post(url, data={"name": "test", "location": "SRID=4326;POINT (158.8025665283203 53.5190837863296)"})
    response.json()
    '''

    # Создаем экземпляр класса сериализатора
    serialinc = ImportIncSerializer(data=request.data)
    if serialinc.is_valid():
        # Если данные валидны, извлекаем данные инцидента и сохраняем их в БД
        valid_data = serialinc.validated_data  # Извлечение валидированных данных один раз
        location_data = valid_data['location'] # Извлекаем данные о местоположении
        name = valid_data['name'] # Извлекаем название точки
        pnt = GEOSGeometry(location_data)  # Создаем объект GEOSGeometry из данных о местоположении
        # Создаем объекты в БД если Данные уже существуют в базе данных, не записываем их повторно
        ImportInc.objects.get_or_create(name=name, location=pnt)
        return Response('Данные переданы службам реагирования! С Вами свяжется оператор.')
    return Response('Данные переданы службам реагирования! С Вами свяжется оператор.') 

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

Настройка взаимодействия клиента с этими представлениями осуществляется в результате процедуры роутинга – задания url-адресов доступа к тем или иным функциям, выполняемой в файле urls.py

urlpatterns = [
    path('create_point/', create_point),
    # Создаем путь к шаблону карты туриста
    path('map/', map_view), 
...]

Для уобства масштабирования проекта в дальнейшем файл urls.py дублируется отдельно в папке проекта и отдельно прописываются продолжение url-адреса для каждого приложения в urls.py папки приложения.

Функции генерируют контент, который рендерится в html-шаблоне, размещенном в папке templates, и визуализируется в окне веб-браузера с применением таблиц стилей CSS.

Если «обернуть» такие функции в специальный декоратор (или создать вместо функций классы наследующие атрибуты встроенных в Django классов джинериков и миксин, что является более современным подходом) они будут способны обрабатывать REST-запросы от клиента. В примере выше применяется декоратор @api_view(['POST']) . В этом случае функция будет формировать ответ Response , содержащий отдаваемую клиенту в ответ на его запрос некоторую информацию.

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

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

Для понимания элементарный пример взаимодействия клиента с сервисом, представлен ниже.

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

// Добавляем маркер
function saveMarkerToDatabase(coordinates, markerName) {
            fetch('https://swan-decent-shrew.ngrok-free.app/api/create_point/', {
                // Задаем метод REST-запроса
                method: 'POST',
                // Формируем хедер запроса
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRFToken': getCookie('csrftoken'), // Добавляем CSRF-токен в заголовок запроса
                },
                // Формируем тело запроса
                body: JSON.stringify({ name: markerName, location: 'SRID=4326;POINT(' + coordinates.lng + ' ' + coordinates.lat + ')' })
            });
}

В момент клика мыши по определенному url на сервер отправляется этот запрос, содержащий в своем теле контент: имя события и данные локации в формате WKT (код системы координат и проекции и координаты точки). Такой запрос, кроме тела, должен содержать служебную информацию в своем заголовке, разрешающую его выполнение, защищающую приложение от внешних атак – CSRF-токен. По умолчанию CSRF-токен хранится в cookie и для его включения в заголовок запроса его необходимо извлечь следующей функцией.

// Функция получения CSRF-токен (он нужен для POST-запрооса) из куки 
function getCookie(name) {
  var cookieValue = null;
  if (document.cookie && document.cookie !== '') {
    var cookies = document.cookie.split(';');
    for (var i = 0; i < cookies.length; i++) {
      var cookie = cookies[i].trim();
      // Проверяем, начинается ли куки с искомого имени
      if (cookie.substring(0, name.length + 1) === (name + '=')) {
        // Извлекаем значение куки
        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
        break;
      }
    }
  }
  return cookieValue;
}

// Получаем CSRF-токен из куки (Это вынесено во внутрь post-запроса)
var csrftoken = getCookie('csrftoken');

К стати не забудьте в секции MIDDLEWARE файла settings.py прописать django.middleware.csrf.CsrfViewMiddleware.

MIDDLEWARE = [..., 'django.middleware.csrf.CsrfViewMiddleware', ...]

Итак запрос отправляется по заданному url. Связанная с этим url функция, ее еще называют эндпоинтом, и мы видели ее выше, успешно отрабатывает о чем свидетельствует статус ответа 200 и сообщение клиенту 'Данные переданы службам реагирования!...'. Если не возникают какие-либо исключения десериализованные и валидные данные о нанесенной на карту точке записываются в базу данных.

Когда пользователь только запускает приложение по url {HOST}/api/map/ стартует функция представления map_view.

# Функция запуска странички с картой
def map_view(request):

    # Забираем из БД предварительно сериализовав данные
    trek_pnt = serialize('geojson', ImportTrek.objects.all(),
            geometry_field='location',
            fields=('name', 'location',))
        
    trek_lines = serialize('geojson', ImportTrekLine.objects.all(),
            geometry_field='location',
            fields=('name', 'azimuth', 'pn', 'distance', 'location',))
    
    try:
        valid_point_trek = json.loads(trek_pnt)
        valid_line_trek = json.loads(trek_lines)
        
    except json.JSONDecodeError:
        return Response({'error': 'No valid GeoJSON data.'}, status=status.HTTP_400_BAD_REQUEST)
    
    context = {
        'context_point_trek': valid_point_trek,
        'context_lines_trek': valid_line_trek,
        }
      
    # передаем контекст в шаблон map.html
    return render(request, 'map.html', context)

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

Например, по ключу "features" во вложенный список помещается геометрия объекта вместе с ее типом "Point" и, по ключу "properties", - параметры атрибутов пространственных объектов, в данном примере это название точки.

{ "context_point_trek": { "type": "FeatureCollection", "crs": { "type": "name", "properties": { "name": "EPSG:4326" } }, 
"features": [ 
{ "type": "Feature", "id": 1, "properties": { "name": "Точка: 1" }, "geometry": { "type": "Point", "coordinates": [ 158.839538, 53.572238 ] } }, 
{ "type": "Feature", "id": 2, "properties": { "name": "Точка: 2" }, "geometry": { "type": "Point", "coordinates": [ 158.83972, 53.572024 ] } }, (и так далее...)

Такой контент в виде переменных передается обратно в html-шаблон, где обрабатывается уже посредствам java-скриптов и стилизованно отображается в окне веб-браузера в виде слоев карты, таблиц, графиков, диаграмм и т.д., совместно с элементами пользовательского интерфейса (рисунок 3). Также такой контент может отдаваться в теле ответа на сформированный POST-запрос, для этого нужно лишь подкорректировать эндпоинт.

// РЕЖИМ ОНЛАЙН
// Так если получаем переменные напрямую из функции обработчика
var trek_points = {{ context_point_trek.features|safe }};
// Наносим точки на карту
var my_trek_points = L.geoJSON(trek_points, {
    onEachFeature: function(feature, layer) {
      map.on('zoomend', function() {
        if (map.getZoom() < 18) {
          layer.unbindTooltip();
        } else {
          layer.bindTooltip(feature.properties.name, { permanent: true, direction: 'top' });
        }
      });
    },
    pointToLayer: function (feature, layer) {
      return L.circleMarker(layer, {
        radius: 6,
        fillColor: "#ff7800",
        color: "#343a40",
        weight: 1,
        opacity: 1,
        fillOpacity: 0.5
      });
    }
  }).addTo(map);
Рисунок – 3. Фрагмент пользовательского интерфейса приложения
Рисунок – 3. Фрагмент пользовательского интерфейса приложения

Для создания карты подложки на основе открытого ресурса OpenStreetMap применяется следующий скрипт html-шаблона.

// Это базовый слой OSM
var baselayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
                        minZoom: 0, // минимальный масштаб
                        maxZoom: 18, // максимальный масштаб
                        tms: false, // использование TMS
                        attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' // ссылка на авторство
}).addTo(map); // добавление тайлов OSM как подложки

Для администрирования приложения, предназначается специальная панель Django, позволяющая создавать и удалять записи из базы данных пользователям, прошедшим процедуры идентификации, аутентификации и авторизации (рисунок 4).

Рисунок – 4. Панель администрирования
Рисунок – 4. Панель администрирования

Для создания пользователя (на начальном этапе) используется команда

python manage.py createsuperuser

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

Подытожить материал этой статьи хочется следующим. Логика веб-ГИС-приложения будет реализовывать инструменты получения спутниковых снимков с открытых ресурсов, например Sentinel Hub, и обработки полученных растров по специальным алгоритмам. Суть этой обработки заключается в комбинировании спектральных каналов спутниковых снимков и расчет спектральных индексов для решения задач на основе технологий машинного обучения: дешифрирования, классификации и кластеризации объектов и явлений местности. В дальнейшем также планируется внедрить в проект на основе этих наработок предобученные нейронные сети. Само собой это материалы для написания еще нескольких статей в дальнейшем. Будьте на связи, кому интересно подписывайтесь. 

В общем такой подход позволит реализовать ряд сервисов, размещенных в облаке, доступ к которым будет осуществляться по подписке. Клиенты будут использовать такие сервисы для конструирования собственных приложений, на подобии Notion.

Рисунок – 5. Менеджер задач служб реагирования
Рисунок – 5. Менеджер задач служб реагирования

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

Еще одним элементом этой инфраструктуры, станет возможность не только мониторить обстановку, но и отдавать распоряжения и контролировать их исполнение различным службам реагирования (рисунок 5), путем реализации дополнительного связанного с базой данных мобильного приложения (Android, IOS, и др. наше приложение платформонезависимо, в этом его прелесть!).

Видеопрезентация полного цикла работы приложения доступна по ссылке.

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