Электронные навигационные карты (ENC) формата S-57 стоят на каждом коммерческом судне в мире. Стандарт живёт с 1996 года, но живого JavaScript-парсера для него я так и не нашёл: либо серверный GDAL, либо проприетарные SDK за шестизначные суммы в год. Я написал полный стек с нуля: бинарный парсер ISO 8211, доменную модель S-57 и S-101, рендер символики S-52 на Canvas2D. Всё бесплатно работает в браузере, без сервера, MIT-лицензия.

Причалы гавани складываются в S-57, слева настоящие байты карты US5MA12M.000
s57-parser: морская карта S-57 в браузере. В коде слева оставил пасхалку

Live demo (порт Бостон, реальная карта NOAA, парсится прямо во вкладке): https://devladpopov.github.io/s57-parser/

Исходники: https://github.com/devladpopov/s57-parser

Зачем это нужно, и почему это особенно актуально у нас

Рынок электронных навигационных карт оценивают в 483 млн долларов в 2025 году с прогнозом до 2.1 млрд к 2032. С января 2026 идёт переход на новый стандарт S-101 (обязательный для новых ECDIS с 2029). А инструментов для разработчика по-прежнему почти нет:

  • Единственный JS-пакет (s57-reader на npm) мёртв, две звезды, читает только метаданные.

  • Все веб-решения тянут серверную конвертацию через GDAL (C++) или коммерческие SDK ценой от 10 до 200 тысяч долларов в год.

  • Ни одного browser-ready рендерера символики S-52 в open source нет.

Отдельно про российский контекст. Крупные поставщики ECDIS-движков это западные компании: SevenCs (сейчас в составе Wärtsilä), Esri, Hexagon. Лицензировать их в России сейчас сложно и дорого. При этом S-57 это международный стандарт IHO, и в этом формате выпускаются в том числе отечественные карты (ГУНиО производит российские ENC). Получается странная ситуация: данные в открытом стандарте есть, а свободного инструмента, чтобы просто прочитать и показать их в вебе, нет.

Мой парсер закрывает ровно эту дыру. Он не зависит от вендора, не ходит в облако, целиком работает на клиенте и читает любую S-57-ячейку независимо от того, кто её выпустил. Это полезно для морских вузов (Макаровка и подобные), для внутренних проектов, где нельзя тащить западный SDK, и просто для того, чтобы посмотреть карту без тяжёлого серверного pipeline.

Что такое S-57 за пять минут

S-57 (IHO Transfer Standard for Digital Hydrographic Data) это международный стандарт обмена данными ENC. Карта лежит в бинарном формате ISO 8211 с расширением .000.

Внутри файла три вида записей:

  • Feature records: объекты реального мира (буи, маяки, изобаты, береговая линия).

  • Spatial records: геометрия (точки, рёбра графа, координаты).

  • Topology: chain-node модель, где рёбра связывают узлы, а полигоны собираются из рёбер.

Координаты целочисленные, с множителем COMF (обычно 10 000 000). Долгота 34.5678 хранится как 345 678 000.

Грабли, на которые я наступил

1. ISO 8211: бинарный формат из девяностых

ISO/IEC 8211 это стандарт 1994 года для структурированных данных. Каждый файл начинается с DDR (Data Descriptive Record), который описывает структуру полей. Дальше идут записи переменной длины со смешанным бинарно-текстовым содержимым.

Главная подстава: два разных синтаксиса описания бинарных полей.

b15   суффиксная нотация: signedness = 1, byteWidth = 5
B(40) скобочная нотация: 40 бит = 5 байт, unsigned

Выглядят почти одинаково, а парсятся совершенно по-разному. Поле NAME в записи FSPT использует B(40). Если разобрать его как суффиксное b40 (signedness = 4, bytes = 0), получаешь 0 байт вместо 5. Все ссылки на геометрию рассыпаются, канвас пустой. Этот баг стоил мне нескольких часов с hex-дампами реальных файлов NOAA.

2. Chain-node топология

S-57 не хранит координаты полигонов напрямую. Вместо этого топологическая модель:

Feature (DEPARE, область глубин 5-10 м)
  -> FSPT ссылается на Edge records
       -> Edge содержит промежуточные координаты (SG2D)
            -> VRPT ссылается на ConnectedNode (начало и конец)

Полигон собирается из нескольких рёбер, каждое ребро начинается и заканчивается в узле. Рёбра могут идти в обратную сторону (ORNT = Reverse), внутренние кольца помечены USAG = Interior.

Ловушка: разные типы пространственных записей (Edge с RCNM = 130, ConnectedNode с RCNM = 120, IsolatedNode с RCNM = 110) могут иметь одинаковый RCID. Если брать RCID ключом в Map, записи затирают друг друга. Решение простое: составной ключ rcnm * 100000 + rcid.

3. Механизм обновлений

Навигационные карты обновляются еженедельно. Файл .001 это первый апдейт, .002 второй, и так далее. Каждый апдейт несёт инструкции: вставить запись, удалить, изменить. Плюс операции на уровне подзаписей: сплайс координат (SGCC), сплайс пространственных ссылок (FSPC), правка узлов рёбер (VRPC). Реализовать это правильно оказалось отдельным приключением.

4. S-52: символика, которую никто не реализовывал в опенсорсе

S-52 Presentation Library это стандарт IHO для отрисовки карт. Он задаёт:

  • три палитры: DAY_BRIGHT, DUSK, NIGHT (примерно по 30 цветовых токенов);

  • правила отображения для 50 с лишним типов объектов;

  • условную символику (цвет области глубин зависит от DRVAL1 и DRVAL2);

  • текстовые метки глубин и характеристик огней (Fl, Q, Iso, Oc);

  • секторные огни (дуги по истинным азимутам);

  • паттерн-заливки (штриховка, пунктир).

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

Архитектура

Проект это Bun-монорепозиторий из семи пакетов:

@s57-parser/iso8211      чистый парсер ISO 8211 (ноль зависимостей)
        |
   +----+----+
   |         |
@s57-parser/s57   @s57-parser/s101
   |         |
   +----+----+
        |
@s57-parser/s52-render   рендерер на Canvas2D
        |
   +----+----+
   |         |
@s57-parser/leaflet  @s57-parser/maplibre

@s57-parser/cli          инструмент командной строки

Всего около 5100 строк TypeScript, 127 тестов, ноль рантайм-зависимостей в core-пакетах.

import { parseS57, toGeoJSON } from '@s57-parser/s57';

const dataset = parseS57(buffer);
const geojson = toGeoJSON(dataset); // обычный GeoJSON, дальше делай что хочешь

Про S-101 стоит сказать отдельно, потому что встречается распространённое заблуждение “S-101 это GML/XML”. Нет. S-101 использует тот же ISO 8211, просто с расширенной моделью данных (комплексные атрибуты, information records, составные кривые). Поэтому мой парсер ISO 8211 покрывает оба формата, а пакет s101 сам определяет формат файла и маппит 160 с лишним типов объектов S-101 обратно в коды S-57 для совместимости с рендерером.

Что появилось, чтобы этим можно было пользоваться без установки

Изначально это была библиотека для разработчиков. Потом я добавил то, что превращает её в готовый инструмент прямо в браузере:

  • Открыть любую карту NOAA. Кидаешь zip exchange-set, парсер сам достаёт базовую ячейку и накатывает на неё апдейты .001 и .002, как это делает настоящий ECDIS.

  • Каталог из 7108 действующих ячеек NOAA с поиском по региону и названию, карта открывается в один клик.

  • Экспорт: разобранные объекты в GeoJSON, текущий вид в PNG или PDF.

Всё на клиенте. Ни установки, ни сервера, ни GDAL.

Результаты в цифрах

Проверял на реальных картах NOAA (они свободно лежат на charts.noaa.gov). Карта бостонской гавани (NOAA US5MA12M):

  • парсинг ISO 8211 плюс построение модели S-57: около 800 мс в браузере;

  • конвертация в GeoJSON: около 60 мс;

  • 2320 объектов, 4661 пространственная запись;

  • рендер S-52 на Canvas2D: реальное время при панорамировании и зуме.

Маленькая ячейка US5MA19M парсится за 5 мс (71 объект), то есть производительность масштабируется линейно с размером файла.

Как попробовать

Демо без установки: https://devladpopov.github.io/s57-parser/

npm install @s57-parser/s57 @s57-parser/s52-render

Скачиваешь тестовую карту с NOAA, кидаешь zip или .000 на канвас. Для Leaflet и MapLibre GL JS есть готовые плагины (@s57-parser/leaflet и @s57-parser/maplibre).

Честный блок: как это писалось

Я собирал этот стек не в одиночку в классическом смысле: значительную часть кода, тестов и отладки я вёл через AI-агентов, но это отдельная история.

Что агенты делали хорошо: рутину. Обвязку тестов, конвертацию структур, генерацию граничных случаев, разбор спецификации на куски. Что делали плохо: как раз те места, где нужно было держать в голове весь бинарный формат сразу. Историю с b15 против B(40) ни один агент сам не поймал, это пришлось ловить руками по hex-дампам. То есть агент отлично экономит время на понятной механике, но на действительно каверзных багах домена всё равно думает человек.

Про экономику, инструменты и грабли работы с агентами я подробнее пишу у себя в канале: @popovvii. Там же про то, во сколько это обошлось по токенам и часам против найма команды.

Ссылки

Буду рад фидбэку от тех, кто работает с ENC: особенно по краевым случаям топологии и по деталям кодирования S-101.

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