
Идея спасти мир и при этом заработать немного шекелей витала у меня в голове уже давно. Имея неплохой накопленный опыт в области геоинформационных систем и защитивши в свое время диссертацию с их применением мне не хватало знаний разработчика. Окончив 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
psqlALTER 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, и др. наше приложение платформонезависимо, в этом его прелесть!).
Видеопрезентация полного цикла работы приложения доступна по ссылке.
