Клиентское приложение для исследования набора геообъектов на карте. Vue 3, TypeScript, Leaflet, KDBush и Web Worker. Набор может быть синтетическим или импортированным GeoJSON, до 500 000 точек. Сервер для данных не нужен: файл читается в браузере, подложка запрашивается у OpenStreetMap.
Нужен Node.js 22.12 или новее.
npm ci
npm run devПриложение открывается на http://127.0.0.1:5173. Сборка: npm run build, затем npm run preview. Статические файлы попадают в dist/. base: './' позволяет открывать сборку из подкаталога.
Экран делится на набор данных, фильтры, список, карту и сводку по текущей выборке.
Синтетический набор задаётся тремя параметрами:
- размер: 10 000, 100 000 или 500 000 объектов;
- seed: целое от 0 до 4 294 967 295, один и тот же seed даёт тот же набор;
- распределение: группы вокруг европейских городов, равномерное по прямоугольнику или одна плотная область.
Кнопка «Применить набор» строит данные заново. Пока идёт подготовка, параметры и фильтры недоступны.
У каждого объекта четыре поля: долгота и широта, тип (пункт выдачи, постамат, отделение), статус (работает или обслуживание) и нагрузка от 0 до 100. Идентификатор — порядковый номер в наборе. В карточке и списке он показывается как номер с единицы.
Кнопка «GeoJSON» принимает локальный файл .json или .geojson размером до 80 МБ и не больше 500 000 точек. Ожидается FeatureCollection с геометрией Point и координатами [lng, lat]. Допустимые пределы: долгота от −180 до 180, широта от −85.05112878 до 85.05112878.
Из properties читаются только category (0–2), status (0–1) и load (0–100). Отсутствующее поле становится 0. Остальные свойства не сохраняются. Ошибка разбора не заменяет уже открытый набор: карта, список, выбранный объект и адресная строка остаются прежними. После неудачного чтения того же файла его можно загрузить ещё раз; слишком большой файл нужно заменить.
Успешный импорт подписывает источник именем файла, отключает параметры генератора и подгоняет карту под короткую дугу, покрывающую все точки. Набор через антимеридиан не растягивается на весь мир. «Вернуть демо» снова строит синтетические данные.
Три фильтра применяются вместе:
- тип объекта или все типы;
- статус или все статусы;
- минимальная нагрузка, от 0 до 100.
Фильтр сразу сужает список, маркеры и сводку. Число рядом с заголовком «Фильтры» — сколько объектов прошло фильтры и попало в текущую область карты.
Три способа показать одну и ту же выборку:
- Кластеры. Объекты в ячейке 72 px складываются в один маркер. Подпись — число объектов. Координата маркера — невзвешенное сферическое среднее членов ячейки. Состав ячейки зависит от края видимой области, поэтому при сдвиге карты пограничный кластер может измениться.
- Плотность. В ячейке 14 px остаётся объект с наибольшей нагрузкой. Это не случайная выборка.
- Точки. Каждый объект рисуется отдельно. Если их больше 20 000, на карте остаются первые 20 000, и появляется предупреждение. Список и счётчики при этом полные.
На точке с нагрузкой от 80 другая заливка. Клик по точке выбирает объект и открывает карточку. Клик по кластеру приближает карту. Кластеры рисуются HTML-маркерами. Переключатель Canvas/SVG меняет только отрисовку отдельных точек и не запускает новый пространственный запрос.
«Закрепить область» запоминает текущий прямоугольник карты. Следующие запросы оставляют только объекты внутри него и внутри видимой области. Повторное нажатие снимает область. Кнопка сброса убирает область и фильтры и возвращает исходный вид.
Если подложка не загрузилась, данные, фильтры и список продолжают работать. Об этом есть отдельное сообщение.
Список показывает объекты текущей выборки, отсортированные по идентификатору. В DOM одновременно не больше 12 строк. Длинный результат делится на страницы по 10 000, чтобы не упираться в ограничение браузера на высоту элемента.
Клавиатура: стрелки вверх и вниз, Home и End. Home и End возвращают к уже выбранной строке, даже если список прокрутили вручную. Если состав выборки изменился, а выбранный объект в нём остался, список открывает его страницу и прокручивает строку в окно. Если объекта больше нет, выбор снимается.
Карточка показывает номер, тип, статус, нагрузку и координаты. Закрытие карточки снимает выбор. Выбор на карте и в списке — один и тот же объект.
Четыре показателя:
- объектов в наборе и время построения индекса;
- объектов в текущей области с учётом фильтров;
- средняя нагрузка и сколько из них сейчас работают;
- сколько элементов реально стоит на карте и каким способом они собраны.
Средняя нагрузка и число работающих считаются по всей выборке, а не по тем маркерам, которые поместились на карту.
«Поделиться видом» записывает в адресную строку центр, масштаб, параметры генератора, фильтры, способ отображения и закреплённую область, затем копирует ссылку. Для импортированного набора в ссылке есть только отметка, что нужен локальный файл: сами точки в URL не попадают. Получатель видит сохранённые положение и фильтры и должен загрузить тот же GeoJSON. Если буфер обмена недоступен, ссылку можно скопировать из адресной строки вручную.
Пока открыт импортированный вид без повторной загрузки файла, приложение прямо говорит, что ждёт GeoJSON.
Перемещение и изменение масштаба собираются в один запрос с паузой 90 мс. Одновременно выполняется не больше одного запроса; если вид успел измениться ещё раз, ждёт только последний.
По умолчанию запрос считает Web Worker. В «Настройках исполнения и ограничения» можно переключить исполнение на основной поток и поиск на полный перебор. Пространственный индекс и полный перебор возвращают одни и те же идентификаторы. Индекс основного потока строится при первом таком запросе, пачками, и не входит в цифру «запрос целиком».
Уже начатый запрос внутри Worker не обрывается. Если к моменту ответа вид устарел, ответ отбрасывается. Ошибка запроса показывается на экране. «Повторить запрос» запускает его снова. Если Worker остановился, та же кнопка предлагает восстановить набор.
Смена набора устроена как транзакция. Новый Worker готовит данные, пока на экране остаётся прежний набор. Ошибка уничтожает только новую попытку. Успех подменяет данные и только потом останавливает старый Worker.
Раздел «Лаборатория» измеряет один и тот же набор в фиксированных условиях. На время прогона карта не принимает перетаскивание, колесо, клавиатуру и кнопки масштаба. Остановка или завершение возвращают управление.
В эксперименте три заранее заданных вида, один прогрев и пять повторов. Фильтры эксперимента не зависят от фильтров исследования. Сравниваются полный перебор и индекс на основном потоке, индекс в Worker, а также обновление слоя Canvas и SVG на одинаковых маркерах.
Отчёт показывает медиану и ближайший p95. По пяти повторам p95 совпадает с максимумом. Рядом видны отдельные составляющие: время самого запроса, агрегации, полного круга с передачей результата и обновления слоя. Ни одна из них не равна времени появления кадра на экране. Счётчик кадров в секунду показывает активность основного потока, а не частоту, с которой видеокарта реально показала изображение.
Если во время прогона скрыть вкладку, измерение прерывается: время в фоне не попадает в отчёт. Готовый отчёт скачивается JSON-файлом.
Команда npm run benchmark:record собирает production-версию, прогоняет Chromium на наборах 10 000, 100 000 и 500 000 и сохраняет JSON и таблицу в docs/results. В автоматическом прогоне запросы тайлов отключены.
npm run lint
npm run format:check
npm run typecheck
npm test
npm run build
npm run check:bundle
npx playwright install chromium
npm run test:e2eGitHub Actions выполняет тот же набор. Суммарный gzip JavaScript, включая Worker, не больше 110 КиБ.
Отдельный профиль памяти импорта на 500 000 точек не входит в обычный прогон. Его включают переменной ATLAS_PROFILE_MEMORY=1, как описано в docs/QUALITY.md.
Пример: public/example.geojson.
| Property | Значения | Если поля нет |
|---|---|---|
| category | 0 — пункт выдачи, 1 — постамат, 2 — отделение | 0 |
| status | 0 — работает, 1 — обслуживание | 0 |
| load | целое от 0 до 100 | 0 |
Синтетические данные не описывают реальные пункты выдачи.
Подробности слоёв, очереди запросов и формата отчёта: docs/ARCHITECTURE.md. Отдельные решения: карта, Worker и индекс, агрегация, граница с сервером. Бюджеты и ограничения проверок: docs/QUALITY.md. Разбор измерений: docs/CASE_STUDY.md.
Нет учётных записей, общей работы нескольких людей и потока обновлений с сервера. Колонки набора есть и в интерфейсе, и в Worker. На очень широком виде индекс не отменяет просмотр всех найденных объектов. Кластер описывает текущий кадр, а не постоянные группы во всём наборе. Подложка нужна из сети и подчиняется условиям провайдера; для работы без сети тайлы не сохраняются.