Идея спасти мир и при этом заработать немного шекелей витала у меня в голове уже давно. Имея неплохой накопленный опыт в области геоинформационных систем и защитивши в свое время диссертацию с их применением мне не хватало знаний разработчика. Окончив IT-курсы и получив доступ к «Святому Граалю знаний» я понял, – пора, и завертелось!
Летом 2024 года мы в составе команды «Arrow» одержали победу, заняв третье место в хакатоне «Лидеры цифровой трансформации» и вошли с нашим проектом в топ-100, став резидентами «Академии инноваторов» у нас появился свой стартапп.
Общая идея такова. Arrow - это платформа для анализа и обработки спутниковых снимков, использующая технологии машинного обучения и нейросетей для мониторинга окружающей среды, строительства и природопользования. Наш продукт помогает бизнесу и государственным структурам автоматизировать выявление экологических нарушений и незаконных построек, обеспечивая более точное и своевременное реагирование. Это в «розовом» будущем, а пока это только проект «Мобильное приложение для управления антропогенной нагрузкой на особо охраняемых природных территориях Камчатского края», занявшее призовое место, хотя и этот результат тоже когда-то был только в мечтах.
Я хочу открыть целый цикл статей в котором постараюсь осветить историю жизненного цикла нашего проекта «Arrow», которая будет писаться на ваших глазах. Здесь будет все: и фронт и бэк и мобильная разработка, будет и деплой в облако. В этих статьях, которые к стати буду писать не только я, но и ребята с моей команды, мы хотим осветить все начиная от создания MVP (минимально жизнеспособный продукт) и заканчивая выводом проекта в продакшн, анализ целевой аудитории и поиск первых клиентов, привлечение первых инвестиций, подбор команды, в общем все этапы через которые нам предстоит пройти для достижения своей цели, - получения интересного и востребованного продукта. Начнем же…
В современных реалиях блокировки доступа ко многим программным продуктам остро встает вопрос о переходе к использованию отечественных или, замещающих их и распространяемых открыто, ресурсам для построения элементов инфраструктуры пространственных данных.
Данная статья освещает практический подход для решения задачи построения Веб-ГИС приложения и сервисов на основе открытых ресурсов и на примере нашего проекта. Основное внимание в ней будет уделено созданию общей структуры проекта и освещению ресурсов на которых он функционирует.
Проект реализован с применением клиент-серверной архитектуры (рисунок 1). Серверная часть включает в себя СУБД для хранения векторных и растровых наборов данных, файловое хранилище растровых тайлов, сервер приложений Gunicorn, обслуживающий сам проект, и веб-сервер Nginx для взаимодействия, через протокол REST full с сервисами приложения, программ клиентов, которыми могут быть десктопные и (или) мобильные и (или) веб-браузеры. Таким образом, весь проект будет размещен в облаке Reg.ru о чем в дальнейшем будет отдельная статья, а пока мы работаем на нашей локальной машине (localhost), с организацией туннелированного доступа с применением Ngrok.
Технологический стек проекта включает в себя следующие свободно распространяемые ресурсы:
СУБД 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);
Для создания карты подложки на основе открытого ресурса OpenStreetMap применяется следующий скрипт html-шаблона.
// Это базовый слой OSM
var baselayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
minZoom: 0, // минимальный масштаб
maxZoom: 18, // максимальный масштаб
tms: false, // использование TMS
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' // ссылка на авторство
}).addTo(map); // добавление тайлов OSM как подложки
Для администрирования приложения, предназначается специальная панель Django, позволяющая создавать и удалять записи из базы данных пользователям, прошедшим процедуры идентификации, аутентификации и авторизации (рисунок 4).
Для создания пользователя (на начальном этапе) используется команда
python manage.py createsuperuser
В дальнейшем, для регистрации новых пользователей приложения, этот функционал будет расширен.
Подытожить материал этой статьи хочется следующим. Логика веб-ГИС-приложения будет реализовывать инструменты получения спутниковых снимков с открытых ресурсов, например Sentinel Hub, и обработки полученных растров по специальным алгоритмам. Суть этой обработки заключается в комбинировании спектральных каналов спутниковых снимков и расчет спектральных индексов для решения задач на основе технологий машинного обучения: дешифрирования, классификации и кластеризации объектов и явлений местности. В дальнейшем также планируется внедрить в проект на основе этих наработок предобученные нейронные сети. Само собой это материалы для написания еще нескольких статей в дальнейшем. Будьте на связи, кому интересно подписывайтесь.
В общем такой подход позволит реализовать ряд сервисов, размещенных в облаке, доступ к которым будет осуществляться по подписке. Клиенты будут использовать такие сервисы для конструирования собственных приложений, на подобии Notion.
Также планируется, возможность организации целой инфраструктуры (как вы наверно уже поняли, в минимально работоспособном варианте это уже реализовано), когда на основе сервисов можно будет создавать панели мониторинга, например мест скопления твердых бытовых отходов, незаконных застроек, зон пожаров и гарей, затоплений и многого другого. Такие дашборды смогут конструировать сами пользователи на основе предварительно созданного инструментария на стороне фронта (карт, таблиц, графиков, диаграмм и др.). Своего рода nocode-приложения для неподготовленных пользователей, либо с элементами кодирования, для более подготовленных, путем использования нашего API. Кроме того, мы сами сможем обеспечить всю цепочку создания, развертывания и поддержки приложения под более конкретные запросы пользователей.
Еще одним элементом этой инфраструктуры, станет возможность не только мониторить обстановку, но и отдавать распоряжения и контролировать их исполнение различным службам реагирования (рисунок 5), путем реализации дополнительного связанного с базой данных мобильного приложения (Android, IOS, и др. наше приложение платформонезависимо, в этом его прелесть!).
Видеопрезентация полного цикла работы приложения доступна по ссылке.