В рамках выпускной квалификационной работы мне предложили две интересные темы: интерактивную карту или тренажёр для SQL-запросов. Я хотел посложнее, чтоб получить побольше навыков и поднабраться опыта к окончанию бакалавриата, поэтому выбрал первое. Получилась небольшая ГИС, полностью написанная на JavaScript при помощи D3.js и Charts.js.

Сразу скажу, что сейчас появилась небольшая проблема с тем, что фреймворк работает черезCloudflare, который блокируетсяРКН.
Из-за чего js-кодне прогружается, потому чтонет доступа к фреймворку. Что поможет в таких случаях?Внутреннее принятие неизбежного.Сейчас все работает вроде хорошо, но если не работает, то читаем зачеркнутый текст.
Забыл представиться, меня зовут Артём, я закончил бакалавриат по направлению «Математика и компьютерные науки» в СГУ им. Питирима Сорокина.
Этот проект, как сказано выше - моя выпускная квалификационная работа, и по совместительству - первое серьёзное веб-приложение, в котором я в одиночку соединил открытые данные, картографию, JavaScript и страдания упорство.
Полный проект на гитхабе.
Чуть расскажу о результате и перейду к тому как реализовывалось.
Функционал
Приложение позволяет интерактивно выбирать районы, просматривать динамику социально-экономических показателей, фильтровать данные и получать справки об объектах. Интерфейс поддерживает тёмную и светлую темы, переключаемые слои и полностью функционирует в браузере без установки.
Интерактивная карта в действии
Ниже — как выглядит карта в светлой и темной теме. На скриншотах ниже отображены административные районы Республики Коми. Это основной уровень, с которого пользователь начинает взаимодействие.


А вот так, после включения слоя (об этом будет позже), показываются интерактивные города/деревни/сёла, что позволяет спуститься на уровень муниципальных поселений.
Можно анализировать показатели по республике в целом, по районам или по конкретным населённым пунктам.уровень муниципальных поселений


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


Реализация
Итак, для визуализации карты я выбрал D3.js.
Конечно, есть более привычные решения вроде Leaflet или Mapbox. Но они работают с тайлами, а мне хотелось чего-то "настоящего" - полного контроля над отрисовкой и максимального погружения.
Да и честно говоря, хотелось немного усложнить себе жизнь - всё-таки диплом.
Данные
Честно сказать, на момент написания диплома, JS я знал только на уровне одного пройденного курса на интернет-платформе, ведь в вузе мы его не касались, но мне помогли упорство и нейросети. Начал заниматься этим в феврале, чтоб точно успеть сделать к июню, и не зря так рано.
Первым делом я начал искать готовые .svg-карты, потому что вообще не понимал, с чего начинается работа с геоданными. Все, что я находил - платные карты, а из бесплатных за первый час нашёл только карту России с делением по регионам.
Позже наткнулся на ресурс Geofabrik, там уже я нашел .shp карты, был очень этому рад. Но за этой радостью стоял один нюанс - карта не республики, а Северо-Западного Федерального Округа.
Понял что придется избавляться от лишнего.
Хотя сперва подумал, что буду работать со всеми регионами и просто поменяю название темы, но рад что отказался от этой идеи, потому что времени бы не хватило.
Позже выяснилось, что с .shp форматом JS не любит работать. Тогда мне посоветовали скачать QGIS.
Вот так выглядел исходный набор данных в QGIS, перед тем как я вырезал всё лишнее:

Набор разбит на карты рек, местностей, болот и т.д.
Я понял, что нужно вырезать все лишнее и оставить только все, что касается республики Коми. Сначала я пытался вырезать всё вручную, но в процессе выделения карты самой республики понял, что это слишком долго.
Пришла идея оптимизировать: я не случайно выше оставил часть про бесплатную карту РФ, потому что при помощи поиска по атрибутам выделил оттуда РК. С её помощью я выделил контур Республики Коми через таблицу атрибутов. Затем в QGIS применил фильтр «Пересечение» к каждому из слоёв СЗФО — оставляя только те объекты, которые попадали внутрь границ Коми.
Так получилось автоматически отфильтровать и собрать отдельный набор геоданных только по нужному региону.

Разработка карты
Дальше я уже начал работать с картой и JS. При помощи нейросеток понял как работать с d3, они помогли накидать макет, чтоб отображать саму карту, разделенную на районы.
Карта
Изначально я хотел, чтобы при большом масштабе отображались все подробные слои, а при отдалении — только карта Республики Коми. Казалось бы, логично. Но на практике такая карта требовала невероятное количество ресурсов: всё тормозило, даже при средней детализации.
Чтобы снизить нагрузку, я пошёл другим путём: стал подгружать только тот район, к которому приближается пользователь. Да, в ГИС-системах это делается автоматически — подгружается только то, что в пределах экрана (например можно было делать это через quadtree
в d3.js), но на тот момент у меня не было понимания как это реализовать, поэтому я выбрал этот вариант.
Теперь я вырезал карты для каждого района снова, используя тот же метод, при помощи таблицы атрибутов выделяя районы и нужные мне для них карты. Я оставил следующие карты : "Границы", "Населенные пункты", "Терминалы", "Болота", "Инфраструктура", "Пляжи", "Парковки", "Церкви", "Здания", "Железные дороги", "Дороги", "Реки".
Получилось всего 240 карт (по 12 карт для 20 районов). Занимаемое место на диске - 1.12гб. Ближе к защите я уменьшил объем данных примерно на 10%, сократив геоданные до 5 знаков после запятой. А после удалил нулевые атрибуты. Итоговый набор карт занимает 359 мб. Я добавил функцию подсветки регионов, а дальше пошла оптимизация отображения и работа над дизайном.
Дизайн
После реализации отображения я сразу приступил к подбору цветов объектов. Я старался не ориентироваться на другие решения а собрать свою личную цветовую гамму. И в этот же момент подумал «как круто будет сделать две темы». Взял за основные цвета зеленый и бежевый для светлой темы, а для темной синий и оранжевый.
Первая задача — мне нужно было сделать логику переключения режима отображения выбранного типа карт. И разработать интерфейс.

Итого: у меня есть кнопка смены темы,
кнопка вк/вык_лючающая все карты,
кнопки переключающие отображение кнопок (в правой части экрана) и иконок (в верхней части экрана), которыми можно вк/вык_лючать определенный тип карт.

Все элементы интерфейса я рисовал вручную в Inkscape, в формате .svg. Хотелось сделать все иконки в минималистичном стиле, но фантазия закончилась после основных. Я же все-таки по образованию математик :(
Переключатели-кнопки (справа) реализованы в виде обычных HTML-кнопок, в них нет чего-то интересного. Нажимаешь - меняют цвет и текст, вк/вык_лючают карты.
А есть переключатели-иконки (сверху), вот они красивые (ну, как минимум старательные), и функциональные: переключают режимы отображения карты, а при нажатии меняют цвет или подсветку, в зависимости от текущей темы. Я пытался сделать их интуитивно понятными для пользователя и некоторые получили интересные детали.
Например, иконка зданий оформлена в виде панельного дома - таких в Сыктывкаре, где я живу, довольно много.
Но есть одна интересная деталь: в верхней части дома я добавил коми орнамент. На иконке он почти незаметен. Но и в реальности такие орнаменты легко упустить из виду, хотя мне они кажутся очень атмосферными и самобытными.
Вот они — мои 48 шедевров SVG-графики:

Для того, чтоб нарисовать эти иконки мне понадобилось чуть больше 24 часов рабочего времени. А теперь можете их брать и вы, хотя я даже не знаю где они могут пригодиться.
А знаете на что у меня ушло столько же времени? А возможно даже больше.
Работа с данными
Я решил подключить Wikidata API, чтоб при нажатии на объект у меня отображались возможные данные с википедии, чтоб было поинтереснее и ограничиться этим. Но научный руководитель мне говорит:
Это намного больше чем я хотел, но все же не то. Нужно добавить социально-экономические показатели у районов.
После этой фразы я приступаю к поиску данных. Я не понимал откуда их брать, подсказать не могли, но потом приглянулся Росстат.
Сразу же - первая подножка: когда я решил загрузить где-то третий по счёту показатель, сайт начал возвращать 404. Я предположил, что либо запросов слишком много, либо таблицы слишком большие. Поэтому начал брать только те таблицы, которые нравились мне самому, а с ними проблем уже не было. Я скачивал .csv-файлы и надеялся, что дальше будет легко. Но иногда всплывало что-то вроде:
«Необходимо, чтобы в заголовке, в шапке и в боковике таблицы был выбран не менее одного признака».
Хотя показатели как-то же должны автоматически там распределять.
В общем к программистам Росстата у меня большой вопрос.
А уж работа с этими .csv-файлами оттуда — просто мука. Мне нужен был унифицированный вид, но его не было. Я писал python‑скрипты, чтоб перевести данные для каждой из таблиц, потому что вид у них был не поддающийся логике. Где‑то значение в той же строке, где‑то в следующей, где‑то через одну и прочие проблемы. Спустя где‑то 25 часов я перевел 17 таблиц в JSON формат в единый вид и приступил к оформлению поля информационных показателей. Конец страданиям. На защите меня спросили: «а почему не использовали парсинг данных с Росстата с какой‑нибудь периодичностью, данные же могут обновляться" - идея конечно крутая, но мне кажется это адом.
Для визуализации самих показателей я подключил Charts.js. Потому что быстро, красиво, удобно, и хотелось, чтоб d3 был для логики карты, а charts для графиков.
Техническая часть
Немного про код: просто скажу что делают функции. Продублирую, что можно (и нужно) посмотреть полный проект на гитхабе.
Основные функции отрисовки и логики карты
render() – отвечает за полную перерисовку карты (отрисовывает фон, районные слои, слои объектов и подсвеченные элементы в зависимости от масштаба и темы).
drawMap(geojsonFile, fillColor, strokeColor, opacity) – загружает главный GeoJSON и запускает
render()
.updateProjection() – подгоняет проекцию D3 под размер канваса, чтобы карта занимала весь экран.
clearMap() – очищает текущие данные районов.
Загрузка и кеширование геоданных
loadDistrictMaps(district, colorScheme) – загружает слои объектов для выбранного района (здания, дороги, леса и т.д.), используя кэш и фильтрацию по зуму.
updateDistrictCenters() – загружает геометрию границ районов, чтобы потом определять ближайший к центру экрана район.
getClosestDistrict() – определяет, какой район сейчас ближе всего к центру карты.
Наведение и взаимодействие
isPointInPolygon(point, polygon) – проверяет, лежит ли точка внутри полигона (используется в
getClosestDistrict()
).addRiverLabel(feature) – отрисовывает название реки и дороги по направлению потока.
capitalizeFirstLetter(str) – делает первую букву строки заглавной (используется для выделенного объекта).
Управление слоями карты и интерфейсом
createMapControls(mapType) – создаёт кнопки управления слоями (иконки и текстовые кнопки).
setButtonState(type, index, state) – переключает видимость слоя и обновляет текст кнопки.
updateMapMode() – проверяет, активен ли какой-либо слой и загружает соответствующие слои района.
updateIconButtonStyles() – обновляет иконки кнопок в зависимости от темы и состояния.
updateButtonStyles() – меняет стили текстовых кнопок в зависимости от темы и активности.
applyBoxShadows() – обновляет цвета подсказок в зависимости от темы.
changeIconColor() – меняет иконки главных кнопок управления (меню, темы и пр.) в зависимости от темы и состояния.
Переключение темы
themeButton.onclick – переключает тёмную/светлую тему, перерисовывает карту, включает нужную цветовую схему.
Интеграция с Wikidata и OSM
fetchWikiData(wikidataID) – загружает данные объекта по Wikidata ID и отображает их в infoBox.
fetchWikiIDfromOSM(osmID) – по OSM ID делает запрос к Overpass API, чтобы найти связанный Wikidata ID и вызвать
fetchWikiData()
.fetchEntityLabel(entityID) – получает название сущности Wikidata по ID (например, тип объекта, столицу и пр.).
updateInfoBoxWiki(entity, wikidataID) – собирает и отображает подробную информацию об объекте в infoBox, включая изображения, флаг, герб и кнопку социальных показателей.
Социальные показатели (данные)
getAvailableFiles() – возвращает список доступных JSON-файлов с социальными показателями.
findFilesContainingRegionName(regionName) – ищет файлы, в которых есть данные по данному региону.
showFileListInSocialTab() – отображает список файлов с показателями для выбранного региона.
loadRegionsData() – загружает JSON-файл с показателями и извлекает все доступные метрики.
findRegionData(regionName) – находит объект данных региона по его названию, учитывая исключения.
showSocialIndicators(regionName) – отображает таблицу и графики с показателями по региону (использует Chart.js).
getChartColors() – возвращает палитру цветов для графиков в зависимости от текущей темы.
Прочее
toggleSocialButton() – показывает/скрывает социальную вкладку с данными.
closeInfoBox() – скрывает окно информации.
Заключение
Чем дольше я сидел над проектом, тем сильнее боялся не успеть, так как приближалось начало сессии. Но в итоге я всё успел: закрыл сессию, сдал диплом за месяц до предзащиты. Формально мой диплом назывался "стартап", но стартап, который нельзя открыть (во всех смыслах) - звучит, мягко говоря, сомнительно. Потенциал коммерциализации проекта передает привет Роскомнадзору.
Собираюсь дальше поступать в магистратуру и искать работу.
В любом случае, я доволен результатом. Надеюсь, статья окажется полезной тем, кто занимается разработкой интерактивных карт.
Я не стал подробно расписывать реализацию — скорее дал "удочку" в виде репозитория на GitHub, а дальше, думаю, вы разберётесь.
Тем, кто включилу себя внутреннее принятие неизбежного : Вот сылочка на саму карту: карта.
На телефон я физически не успел адаптировать, а сейчас уже не вижу смысла в этом, но можно включить «версию для пк», там будет уже поприятнее.
Буду рад обратной связи в комментариях.