Initial commit: v1.0
This commit is contained in:
5
.claude/settings.json
Normal file
5
.claude/settings.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"superpowers@superpowers-dev": true
|
||||
}
|
||||
}
|
||||
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
output
|
||||
145
README.md
Normal file
145
README.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# SandCastle
|
||||
|
||||
Инструмент для массового захвата Google Photorealistic 3D Tiles в формате датасета **UAV-GeoLoc**. Через Cesium снимает ортофото-кропы сцены (DB) и «дроновые» кадры (query) по сетке, формирует JSON траектории камеры и сохраняет всё в дерево папок, совместимое с UAV-GeoLoc.
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
1. Установите Node.js 14+.
|
||||
2. Запустите `start.bat`:
|
||||
- `start.bat` — запуск на порту `3000`
|
||||
- `start.bat 3001` — запуск на указанном порту
|
||||
3. Откройте `http://localhost:<порт>` если браузер не открылся сам.
|
||||
|
||||
## Рабочий процесс
|
||||
|
||||
1. **Панель управления** ([index.html](index.html)) — задайте имена сцены и параметры:
|
||||
- `Dataset / Country / City / Region` — 4 уровня дерева в `output/`
|
||||
- `Точка 1 / Точка 2` — противоположные углы прямоугольника в формате DMS
|
||||
- `Шаг сетки (метры)` — размер одного тайла
|
||||
- `Прореживание` — 1 = все точки, 2 = каждая вторая
|
||||
- `DB crop sizes` — ровно 11 уникальных размеров PNG-кропов (через запятую)
|
||||
- `Разрешение query кадра` — размер итогового JPEG (по умолчанию 512)
|
||||
- `FOV вертикальный` — параметр съёмки query-камеры
|
||||
- `Pitch query` — фиксирован в `-90` (top-down)
|
||||
- `Высоты (3 слота)` — чекбоксы + редактируемые значения высот
|
||||
- `Абсолютная высота` — выкл = над рельефом
|
||||
- `Предзагрузка тайлов` — прогрев Cesium перед съёмкой
|
||||
- `Пропускать пустые зоны` — не сохранять области без 3D-контента
|
||||
2. **Сохранить настройки** → `settings.json`.
|
||||
3. **Создать папки** → дерево сцены и `{region}.txt` с углами прямоугольника.
|
||||
4. **Запустить захват** — откроется два окна:
|
||||
- **Карта зоны** ([mapview.html](mapview.html)) — чистый canvas без внешних тайлов, красный прямоугольник зоны, синие точки тайлов, жёлтая активная точка, зелёный след пройденного
|
||||
- **Окно захвата** ([capture.html](capture.html)) — Cesium Viewer, прогресс и лог
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
SandCastle/
|
||||
├── start.bat Запуск (проверяет Node, принимает порт, стартует сервер)
|
||||
├── package.json
|
||||
├── server.js Express backend + API
|
||||
├── index.html Панель управления
|
||||
├── ui.js UI-контроллер панели
|
||||
├── style.css
|
||||
├── capture.html Окно захвата (Cesium)
|
||||
├── mapview.html Окно карты (canvas, без внешних тайлов)
|
||||
├── settings.json Последние настройки (создаётся автоматически)
|
||||
└── output/ Результаты (создаётся автоматически)
|
||||
└── {dataset}/{country}/{city}/{region}/
|
||||
├── {region}.txt Углы зоны: maxLat minLon / minLat maxLon
|
||||
├── DB/
|
||||
│ ├── merge.tif Базовый TIFF сцены (для внешней нарезки)
|
||||
│ ├── db_postion.txt Строки: filename lon lat res_x res_y
|
||||
│ └── img/
|
||||
│ └── crop_{col}_{row}_{size}.png Ортофото по сетке (X=col, Y=row, size=px)
|
||||
├── positive.json Соответствия query -> точный DB crop
|
||||
├── semi_positive.json Соответствия query -> соседние DB crop
|
||||
└── query/
|
||||
└── height{H}_rot{R}/
|
||||
├── height{H}_rot{R}.json Траектория камеры
|
||||
└── footage/
|
||||
└── height{H}_rot{R}_{NN}.jpeg Дроновые кадры
|
||||
```
|
||||
|
||||
## Формат траектории (UAV-GeoLoc)
|
||||
|
||||
`query/height100_rot0/height100_rot0.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "height100_rot0",
|
||||
"width": 512,
|
||||
"height": 512,
|
||||
"frameRate": 30,
|
||||
"numFrames": 75,
|
||||
"durationSeconds": 2.5,
|
||||
"cameraFrames": [
|
||||
{
|
||||
"position": { "x": 3843512.1, "y": 604821.7, "z": 5095123.4 },
|
||||
"rotation": { "x": -60, "y": 0, "z": 0 },
|
||||
"coordinate": { "latitude": 52.41, "longitude": 9.65, "altitude": 100 },
|
||||
"fovVertical": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`numFrames` и `durationSeconds` вычисляются автоматически по фактическому числу сохранённых кадров.
|
||||
|
||||
- `position` — ECEF (Earth-Centered Earth-Fixed) в метрах, получено через `Cesium.Cartesian3.fromDegrees`
|
||||
- `rotation` — `x` = pitch, `y` = heading (yaw), `z` = roll, все в градусах (pitch задаётся в UI)
|
||||
- `coordinate` — геодезические координаты точки съёмки
|
||||
- `fovVertical` — вертикальный FOV камеры Cesium в градусах (задаётся в UI)
|
||||
|
||||
## API сервера
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| GET | `/api/get-settings` | Текущие настройки (или дефолты) |
|
||||
| POST | `/api/save-settings` | Сохранить настройки в `settings.json` |
|
||||
| POST | `/api/create-folders` | Создать дерево сцены и `{region}.txt` |
|
||||
| POST | `/api/start-capture` | Сохранить настройки и вернуть URL `capture.html` |
|
||||
| POST | `/api/save-db-crop` | Сохранить `crop_{col}_{row}_{size}.png` + строку в `db_postion.txt` |
|
||||
| POST | `/api/save-merge-tif` | Сохранить `DB/merge.tif` |
|
||||
| POST | `/api/save-drone-frame`| Сохранить кадр `height{H}_rot{R}_{NN}.jpeg` |
|
||||
| POST | `/api/save-trajectory` | Сохранить `height{H}_rot{R}.json` |
|
||||
| POST | `/api/save-annotations` | Сохранить `positive.json` и `semi_positive.json` |
|
||||
|
||||
## Координаты DMS
|
||||
|
||||
Формат: `52°25'10.28"N 9°39'10.04"E`. Точки 1 и 2 — противоположные углы прямоугольника; их порядок не важен, сервер сам вычисляет `minLat/maxLat/minLon/maxLon`.
|
||||
|
||||
## Параметры съёмки (актуально)
|
||||
|
||||
- `DB crop sizes` — обязательный список из **11 уникальных** положительных размеров (px)
|
||||
- стандартная кнопка в UI подставляет:
|
||||
- `100,150,200,250,300,400,500,512,600,800,1000`
|
||||
- для каждого тайла сохраняется 11 PNG-кропов:
|
||||
- `crop_{col}_{row}_{size}.png`
|
||||
- `Query`:
|
||||
- `drone_size` — размер JPEG кадра
|
||||
- `query_fov_deg` — вертикальный FOV
|
||||
- `query_pitch_deg` — зафиксирован на `-90` (top-down)
|
||||
- высоты query задаются тремя редактируемыми слотами (`height_100_value`, `height_125_value`, `height_150_value`) и включаются соответствующими чекбоксами
|
||||
- при завершении захвата сохраняются:
|
||||
- `DB/merge.tif`
|
||||
- `positive.json`
|
||||
- `semi_positive.json`
|
||||
|
||||
## Обмен окнами
|
||||
|
||||
`capture.html` ↔ `mapview.html` общаются через `BroadcastChannel('sandcastle-map')`. Сообщения:
|
||||
- `{type: 'init', bounds, tiles}` — одноразово после генерации сетки
|
||||
- `{type: 'position', index, total, lat, lon}` — перед съёмкой каждого тайла
|
||||
- `{type: 'done', total}` — по завершении
|
||||
- `{type: 'request-init'}` — карта запрашивает реплей при позднем открытии
|
||||
|
||||
## Требования
|
||||
|
||||
- Node.js 14+
|
||||
- Cesium Ion token (Google Photorealistic 3D Tiles, asset `2275207`) — вшит в `capture.html`
|
||||
- Интернет для Cesium CDN и Google 3D Tiles (карта зоны работает офлайн)
|
||||
|
||||
## Версия
|
||||
|
||||
1.0.0 — Мороз Е.С. Павленко Б. В. / ЛИСАД
|
||||
688
UAV-GeoLoc_Dataset_Analysis.md
Normal file
688
UAV-GeoLoc_Dataset_Analysis.md
Normal file
@@ -0,0 +1,688 @@
|
||||
```
|
||||
АНАЛИЗ ДАТАСЕТА: World-UAV (UAV-GeoLoc)
|
||||
```
|
||||
Дата анализа: 2026-04-06
|
||||
Метод: Полный эмпирический анализ данных на диске + GitHub-репозиторий + проектная
|
||||
страница авторов
|
||||
Все пробелы из предыдущего анализа закрыты реальными данными.
|
||||
1. МЕТАДАННЫЕ
|
||||
Поле Значение
|
||||
Полное
|
||||
название
|
||||
UAV-GeoLoc: A Large-Vocabulary Dataset and Geometry-Transformed
|
||||
Method for UAV Geo-Localization
|
||||
Авторы Rouwan Wu, Jiacheng Deng, Mingyu Mou, Xingyi He, Maojun Zhang, Yu
|
||||
```
|
||||
Liu, Shen Yan (NUDT + Zhejiang University)
|
||||
```
|
||||
```
|
||||
Год, Venue 2025, IEEE Robotics and Automation Letters (RA-L), IF ~5.2, Q1 Robotics
|
||||
```
|
||||
DOI 10.1109/LRA.2025.3588061
|
||||
```
|
||||
Код https://github.com/RingoWRW/GeoLoc-UAV (Python, 17 stars)
|
||||
```
|
||||
Данные https://huggingface.co/datasets/RingoWRW97/UAV-GeoLoc
|
||||
```
|
||||
Лицензия CC BY-NC 4.0 (модификация и коммерческое использование
|
||||
```
|
||||
```
|
||||
запрещены)
|
||||
```
|
||||
Общий
|
||||
объём на
|
||||
диске
|
||||
```
|
||||
~181 GB (Country: ~84 GB, Terrain: ~95 GB, Rot: ~1.4 GB, Index +
|
||||
```
|
||||
```
|
||||
weights: ~0.3 GB)
|
||||
```
|
||||
2. ОБЩАЯ СТАТИСТИКА
|
||||
2.1. Сводка по подмножествам
|
||||
Параметр Country Terrain Rot ИТОГО
|
||||
Категорий верхнего уровня 11 стран 27 типов ландшафта 1 сцена —
|
||||
Городов / под-категорий 35 27 1 63
|
||||
```
|
||||
Сцен (регионов) 171 200 1 372
|
||||
```
|
||||
Параметр Country Terrain Rot ИТОГО
|
||||
```
|
||||
DB crop-изображений (спутник) 141,045 132,990 648 274,683
|
||||
```
|
||||
```
|
||||
Query-кадров (дрон) 308,352 337,704 6,688 652,744
|
||||
```
|
||||
Всего изображений 449,397 470,694 7,336 927,427
|
||||
Avg positives / frame 1.00 1.00 1.00 1.00
|
||||
Avg semi-positives / frame 2.87 2.80 — ~2.83
|
||||
Уникальных размеров crop 6 9 1 11
|
||||
```
|
||||
2.2. Разбиение train / val / test (Index-файлы, только Terrain split)
|
||||
```
|
||||
Split Сцен Query-изображений DB-изображений Уник. labels
|
||||
Train 140 206,108 94,709 9,370
|
||||
Val 40 62,368 26,597 2,757
|
||||
Test 20 33,472 11,684 1,439
|
||||
Итого 200 301,948 132,990 13,566
|
||||
```
|
||||
Дополнительные splits: *_country (180/60/30 сцен), *_all (319/100/50 сцен
|
||||
```
|
||||
```
|
||||
объединённо).
|
||||
```
|
||||
2.3. Распределение positives на query
|
||||
```
|
||||
Из train_query.txt (206,108 строк):
|
||||
```
|
||||
Positives на query Количество Доля
|
||||
1 ~2,500 1.2%
|
||||
2 ~4,800 2.3%
|
||||
3 ~2,200 1.1%
|
||||
4 ~194,558 94.4%
|
||||
5 ~100 <0.1%
|
||||
6 ~1,950 0.9%
|
||||
```
|
||||
Подавляющее большинство query имеют ровно 4 positive DB-патча (1 точный + 3 semi-
|
||||
```
|
||||
```
|
||||
positive соседа из-за 50% overlap нарезки).
|
||||
```
|
||||
3. ИЕРАРХИЯ СЦЕН
|
||||
3.1. Country: 11 стран, 35 городов, 171 сцена
|
||||
Страна Городов Сцен Примеры городов
|
||||
USA 6 29 Boston, Chicago, Los Angeles, New York, San Francisco,
|
||||
Washington
|
||||
```
|
||||
Australia 3 20 Adelaide (7), Melbourne (6), Sydney (7)
|
||||
```
|
||||
```
|
||||
English 4 20 Birmingham (5), Edinburgh (5), London (5), Manchester (5)
|
||||
```
|
||||
```
|
||||
German 4 19 Berlin (5), Falakford (5), Hamburg (4), Munich (5)
|
||||
```
|
||||
```
|
||||
Italy 4 19 Milan (5), Rome (5), Turin (5), Venice (4)
|
||||
```
|
||||
```
|
||||
French 3 13 Lyons (5), Marseilles (4), Paris (4)
|
||||
```
|
||||
```
|
||||
Japan 3 13 Kyoto (4), Osaka (4), Tokyo (5)
|
||||
```
|
||||
```
|
||||
Korea 2 10 Busan (5), Seoul (5)
|
||||
```
|
||||
```
|
||||
Poland 2 10 Krakow (5), Warsaw (5)
|
||||
```
|
||||
```
|
||||
Spain 2 10 Barcelona (5), Madrid (5)
|
||||
```
|
||||
```
|
||||
Brazil 2 8 Rio de Janeiro (3), Sao Paulo (5)
|
||||
```
|
||||
```
|
||||
Замечание: 17 сцен (Edinburgh 5, London 5, Manchester 5, Birmingham/JewelleryQuarter
|
||||
```
|
||||
```
|
||||
1, Chicago/__MACOSX 1) не содержат DB-кропов и positive.json — неполные данные
|
||||
```
|
||||
или артефакты архивации.
|
||||
3.2. Terrain: 27 типов ландшафта, 200 сцен
|
||||
Тип ландшафта Сцен Тип ландшафта Сцен
|
||||
Hill 18 Finca 11
|
||||
Mountain 18 Plateau 11
|
||||
Fall 12 Volcano 11
|
||||
Plain 12 Island 10
|
||||
Basin 11 Gorge 9
|
||||
Delta 11 Wetland 8
|
||||
Calcification 6 Prairie 5
|
||||
Desert 6 Terrace 5
|
||||
Flowers 6 Farm 5
|
||||
Тип ландшафта Сцен Тип ландшафта Сцен
|
||||
Oasis 6 Glacier 5
|
||||
StoneForest 6 Snow 3
|
||||
Pasture 2 Danxia 1
|
||||
Hylare 1 Karst 1
|
||||
3.3. Rot: 1 сцена, 88 вариантов
|
||||
```
|
||||
Сцена: SouthernSuburbs (Аделаида, Австралия)
|
||||
```
|
||||
4. ГЕОГРАФИЧЕСКИЙ ОХВАТ
|
||||
Параметр Значение
|
||||
```
|
||||
Диапазон долгот от -155° (Гавайи) до +176° (Новая Зеландия)
|
||||
```
|
||||
```
|
||||
Диапазон широт от -50° (Патагония) до +63° (Гамбург)
|
||||
```
|
||||
```
|
||||
Континентов 6 (Европа, Азия, Северная/Южная Америка, Австралия, Африка —
|
||||
```
|
||||
```
|
||||
через Terrain)
|
||||
```
|
||||
Климатических
|
||||
зон
|
||||
Множественные: тропики, субтропики, умеренная, субарктика
|
||||
Ключевое преимущество: Первый UAV-to-satellite датасет с мультистрановым
|
||||
```
|
||||
покрытием (6+ стран и все континенты кроме Антарктиды).
|
||||
```
|
||||
5. МЕТОДОЛОГИЯ СБОРА ДАННЫХ
|
||||
```
|
||||
height100: 72 варианта — поворот каждые 5 градусов (0, 5, 10, ..., 355)
|
||||
```
|
||||
height125/150: по 8 вариантов — поворот каждые 45 градусов
|
||||
Итого: 88 вариантов x 76 кадров = 6,688 query-кадров
|
||||
```
|
||||
DB: 648 кропов (27x24 сетка, 200x200 px)
|
||||
```
|
||||
5.1. Источники изображений
|
||||
```
|
||||
Дроновые виды (query):
|
||||
```
|
||||
Параметр Значение
|
||||
```
|
||||
Платформа Синтетический рендеринг (Google Earth Studio 3D)
|
||||
```
|
||||
```
|
||||
Тип съёмки RGB, top-down (камера вертикально вниз, pitch ~90°)
|
||||
```
|
||||
```
|
||||
Разрешение кадров 512x512 px (подтверждено из JSON и реальных файлов)
|
||||
```
|
||||
```
|
||||
FOV 30° по вертикали (из camera trajectory JSON)
|
||||
```
|
||||
Частота кадров 30 fps, 2.5 сек на траекторию
|
||||
```
|
||||
Кадров на траекторию 76 (75 кадров анимации + начальный)
|
||||
```
|
||||
Формат JPEG
|
||||
```
|
||||
Спутниковые виды (gallery / DB):
|
||||
```
|
||||
Параметр Значение
|
||||
```
|
||||
Платформа Спутник (Google Earth / Google Maps tiles)
|
||||
```
|
||||
```
|
||||
Формат PNG (кропы), GeoTIFF (merge.tif)
|
||||
```
|
||||
Разрешение
|
||||
кропов
|
||||
```
|
||||
от 100x100 до 1000x1000 px (11 уникальных размеров, см.
|
||||
```
|
||||
```
|
||||
раздел 6)
|
||||
```
|
||||
```
|
||||
GSD Варьируется по сценам (зависит от zoom level при рендеринге)
|
||||
```
|
||||
5.2. Параметры съёмки дронов
|
||||
Параметр Значение
|
||||
```
|
||||
Высоты полёта 100 м, 125 м, 150 м (3 значения)
|
||||
```
|
||||
Азимуты камеры
|
||||
```
|
||||
(heading)
|
||||
```
|
||||
```
|
||||
0°, 45°, 90°, 135°, 180°, 225°, 270°, 315° (8 направлений, шаг
|
||||
```
|
||||
```
|
||||
45°)
|
||||
```
|
||||
Число вариантов на
|
||||
сцену
|
||||
```
|
||||
24 (3 высоты x 8 азимутов) для Country; 8-24 для Terrain
|
||||
```
|
||||
```
|
||||
FOV камеры 30° (вертикальный, подтверждено)
|
||||
```
|
||||
5.3. Примеры данных
|
||||
Пары query-positive:
|
||||
Влияние высоты на query-изображение:
|
||||
Влияние поворота камеры:
|
||||
6. ПРОСТРАНСТВЕННАЯ НАРЕЗКА СПУТНИКОВЫХ
|
||||
ПАТЧЕЙ
|
||||
6.1. Обнаруженные параметры нарезки
|
||||
Эмпирически установлено путём попиксельного сопоставления кропов с merge.tif:
|
||||
```
|
||||
Правило: stride = crop_size / 2 (ровно 50% overlap) для ВСЕХ сцен без
|
||||
```
|
||||
исключений.
|
||||
Параметр
|
||||
нарезки
|
||||
Значение
|
||||
Стратегия Sliding window с фиксированным overlap
|
||||
```
|
||||
Overlap 50% (stride = crop_size / 2)
|
||||
```
|
||||
```
|
||||
Именование crop_X_Y.png — X по ширине (col), Y по высоте (row)
|
||||
```
|
||||
Позиция в
|
||||
merge.tif
|
||||
merge[Y*stride : Y*stride+crop_size, X*stride :
|
||||
X*stride+crop_size]
|
||||
6.2. Распределение размеров кропов
|
||||
```
|
||||
Crop size (px) Stride (px) Сцен Country Сцен Terrain Итого сцен
|
||||
```
|
||||
100x100 50 1 0 1
|
||||
150x150 75 ~20 11 ~31
|
||||
200x200 100 ~120 111 ~231
|
||||
250x250 125 ~15 0 ~15
|
||||
300x300 150 0 10 10
|
||||
400x400 200 1 3 4
|
||||
500x500 250 0 1 1
|
||||
512x512 256 4 43 47
|
||||
600x600 300 0 8 8
|
||||
800x800 400 0 8 8
|
||||
1000x1000 500 0 4 4
|
||||
```
|
||||
Наиболее частый размер: 200x200 px (231 сцена, ~64% всех сцен).
|
||||
```
|
||||
6.3. Размеры merge.tif
|
||||
Размеры merge.tif сильно варьируются:
|
||||
7. АННОТАЦИИ И МЕТАДАННЫЕ
|
||||
7.1. Файлы аннотаций на сцену
|
||||
Файл Содержание Формат
|
||||
positive.json Точное GT-соответствие: frame_idx -
|
||||
> [crop_name]
|
||||
JSON, 1 positive на
|
||||
кадр
|
||||
Минимум: ~1280x2560 px
|
||||
```
|
||||
Типичный (Country): ~2816x2304 — 3072x2560 px
|
||||
```
|
||||
```
|
||||
Типичный (Terrain, 512 crop): ~7680x5120 — 7680x7680 px
|
||||
```
|
||||
```
|
||||
Максимум: ~15360x11520 px (отдельные сцены Terrain)
|
||||
```
|
||||
```
|
||||
USA/Washington: ~10752x9984 px (512x512 кропы)
|
||||
```
|
||||
Файл Содержание Формат
|
||||
semi_positive.json Частичные перекрытия: frame_idx ->
|
||||
[crop_1, crop_2, ...]
|
||||
JSON, 1-5 semi-
|
||||
positives
|
||||
db_postion.txt GPS-координаты каждого crop filename lon lat
|
||||
res_x res_y
|
||||
```
|
||||
height{H}_rot{R}.json Траектория камеры для каждого
|
||||
```
|
||||
query-варианта
|
||||
JSON с ECEF, lat/lon,
|
||||
FOV
|
||||
merge.tif Полный спутниковый снимок
|
||||
региона
|
||||
GeoTIFF
|
||||
```
|
||||
{RegionName}.txt Границы региона (start/end
|
||||
```
|
||||
```
|
||||
координаты)
|
||||
```
|
||||
Text
|
||||
7.2. Структура camera trajectory JSON
|
||||
7.3. Типы аннотаций
|
||||
Тип аннотации Наличие Комментарий
|
||||
GPS-координаты Да Из Google Earth, точность ~1-3 м
|
||||
```
|
||||
Высота дрона (altitude) Да 100, 125, 150 м (дискретно)
|
||||
```
|
||||
```
|
||||
Азимут камеры (heading) Да 8 значений с шагом 45°
|
||||
```
|
||||
FOV камеры Да 30° вертикальный
|
||||
```
|
||||
Pitch / Roll Да Фиксированный (top-down, ~90°)
|
||||
```
|
||||
Positive/Semi-positive pairs Да positive.json + semi_positive.json
|
||||
Семантические метки Частично 27 категорий terrain type
|
||||
```
|
||||
{
|
||||
```
|
||||
"name": "height100_rot0",
|
||||
"width": 512, "height": 512,
|
||||
"frameRate": 30,
|
||||
"numFrames": 75,
|
||||
"durationSeconds": 2.5,
|
||||
"cameraFrames": [
|
||||
```
|
||||
{
|
||||
```
|
||||
```
|
||||
"position": {"x": ..., "y": ..., "z": ...},
|
||||
```
|
||||
```
|
||||
"rotation": {"x": ..., "y": ..., "z": ...},
|
||||
```
|
||||
```
|
||||
"coordinate": {"latitude": ..., "longitude": ..., "altitude": 150},
|
||||
```
|
||||
"fovVertical": 30
|
||||
```
|
||||
}, ...
|
||||
```
|
||||
]
|
||||
```
|
||||
}
|
||||
```
|
||||
Тип аннотации Наличие Комментарий
|
||||
Временная метка Нет —
|
||||
Depth maps Нет —
|
||||
Segmentation masks Нет —
|
||||
Bounding boxes Нет —
|
||||
8. ПРЕДОБУЧЕННЫЕ МОДЕЛИ
|
||||
```
|
||||
Три checkpoint'а (~95 MB каждый), идентичная архитектура:
|
||||
```
|
||||
Файл Обучение Параметров Описание
|
||||
weights_terrain.pth Terrain split 24,873,938 Только природные сцены
|
||||
weights_city.pth Country split 24,873,938 Только городские сцены
|
||||
```
|
||||
weights_e6_all.pth All (combined) 24,873,938 Объединённый датасет
|
||||
```
|
||||
```
|
||||
Архитектура модели (221 слой):
|
||||
```
|
||||
Архитектуры из GitHub-репозитория:
|
||||
Вариант Backbone Local Features Aggregation Rotation Inv.
|
||||
```
|
||||
Group (основной) DINOv2 ViT-S/14 GroupNet MulConvAP Да (8 углов)
|
||||
```
|
||||
Group-DINOv2 DINOv2 ViT-B/14 GroupNet ConvAP Да
|
||||
Vanilla ResNet50 Global only ConvAP/GeM Нет
|
||||
```
|
||||
AnyLoc (baseline) DINOv2 ViT-G/14 Global GeM Нет
|
||||
```
|
||||
9. ПРОТОКОЛ ОЦЕНКИ
|
||||
logit_scale — обучаемый скаляр температуры для contrastive loss
|
||||
groupnet.extractor — DINOv2 ViT-S/14 с conv projection
|
||||
groupnet.embedder — embedding head
|
||||
aggregator.AAP — adaptive average pooling
|
||||
```
|
||||
aggregator.channel_pool_{1,3,5} — multi-scale channel pooling (MulConvAP)
|
||||
```
|
||||
9.1. Задача
|
||||
```
|
||||
Image-to-image retrieval (drone -> satellite). Дроновое изображение (query)
|
||||
```
|
||||
```
|
||||
сопоставляется с гео-привязанными спутниковыми патчами (gallery) через ранжирование
|
||||
```
|
||||
по сходству эмбеддингов.
|
||||
Ключевое отличие от University-1652: One-to-N matching — одному query может
|
||||
соответствовать несколько positive и semi-positive gallery-патчей.
|
||||
9.2. Метрики
|
||||
Метрика Используется Комментарий
|
||||
Recall@1 Да Стандарт для всех CVGL-работ
|
||||
Recall@5 Да —
|
||||
Recall@10 Да —
|
||||
9.3. Результаты на Rot-подмножестве
|
||||
```
|
||||
"Fire" — модели, обученные на World-UAV; "Box" — модели с LPN-методом.
|
||||
```
|
||||
10. ПОЛНАЯ СТРУКТУРА ДАННЫХ
|
||||
UAV-GeoLoc/ # ~181 GB
|
||||
├── Country/ # ~84 GB, 11 стран, 35 городов, 171
|
||||
сцена
|
||||
```
|
||||
│ └── {Country}/{City}/{Region}/
|
||||
```
|
||||
│ ├── DB/
|
||||
```
|
||||
│ │ ├── merge.tif # полный спутниковый снимок (GeoTIFF)
|
||||
```
|
||||
│ │ ├── db_postion.txt # GPS: filename lon lat res_x res_y
|
||||
│ │ └── img/
|
||||
```
|
||||
│ │ └── crop_X_Y.png # нарезанные патчи (stride =
|
||||
```
|
||||
```
|
||||
crop_size/2)
|
||||
```
|
||||
│ ├── query/
|
||||
```
|
||||
│ │ └── height{H}_rot{R}/ # 24 варианта (H: 100/125/150, R: 0-315)
|
||||
```
|
||||
│ │ ├── footage/
|
||||
```
|
||||
│ │ │ └── height{H}_rot{R}_{frame}.jpeg # 76 кадров, 512x512
|
||||
```
|
||||
```
|
||||
│ │ ├── height{H}_rot{R}.json # траектория камеры
|
||||
```
|
||||
```
|
||||
│ │ └── height{H}_rot{R}.esp # Earth Studio project
|
||||
```
|
||||
│ ├── positive.json # GT: frame_idx -> [exact crop match]
|
||||
│ ├── semi_positive.json # frame_idx -> [overlapping crops]
|
||||
```
|
||||
│ └── {Region}.txt # координаты региона
|
||||
```
|
||||
│
|
||||
├── Terrain/ # ~95 GB, 27 типов, 200 сцен
|
||||
```
|
||||
│ └── {TerrainType}/{Scene}/ # структура аналогична Country
|
||||
```
|
||||
│ ├── DB/
|
||||
│ ├── query/ # 8-24 варианта
|
||||
│ ├── positive.json
|
||||
│ ├── semi_positive.json
|
||||
```
|
||||
│ └── visible_gt/ # (опционально) GT видимости
|
||||
```
|
||||
│
|
||||
├── Rot/ # ~1.4 GB, 1 сцена
|
||||
```
|
||||
│ └── SouthernSuburbs/ # 88 вариантов (72 rot@h100 + 8@h125 +
|
||||
```
|
||||
```
|
||||
8@h150)
|
||||
```
|
||||
│ ├── DB/ # 648 кропов
|
||||
│ └── query/
|
||||
│
|
||||
├── Index/ # train/val/test splits
|
||||
```
|
||||
│ ├── train.txt / val.txt / test.txt # списки сцен (Terrain)
|
||||
```
|
||||
```
|
||||
│ ├── train_country.txt / val_country.txt / ...# списки сцен (Country)
|
||||
```
|
||||
│ ├── train_all.txt / val_all.txt / ... # объединённые
|
||||
│ ├── train_query.txt # 206K строк: path label pos1
|
||||
[pos2...]
|
||||
│ ├── train_db.txt # 95K DB-путей
|
||||
│ ├── val_query.txt / val_db.txt # 62K / 27K
|
||||
│ └── test_query.txt / test_db.txt # 33K / 12K
|
||||
│
|
||||
├── weights_terrain.pth # 95 MB, 24.9M params
|
||||
├── weights_city.pth # 95 MB, 24.9M params
|
||||
11. СРАВНЕНИЕ С КЛЮЧЕВЫМИ ДАТАСЕТАМИ
|
||||
Характеристика World-UAV University-1652 SUES-200 VIGOR CVUSA
|
||||
Год 2025 2020 2023 2021 2019
|
||||
Venue RA-L ACM MM — CVPR —
|
||||
Вид пары D<->S D<->S<->G D<->S G<->S G<->S
|
||||
Число локаций 372 1,652 200 90K+ 44K
|
||||
Число
|
||||
изображений
|
||||
927K 72K+ 4,800 238K 88K
|
||||
```
|
||||
Число стран 6+ 1 (Китай) 1 (Китай) 1
|
||||
```
|
||||
```
|
||||
(США)
|
||||
```
|
||||
```
|
||||
1 (США)
|
||||
```
|
||||
Число типов
|
||||
сцен
|
||||
```
|
||||
27 ~1 (кампус) ~1
|
||||
```
|
||||
```
|
||||
(кампус)
|
||||
```
|
||||
смеш. смеш.
|
||||
Разрешение
|
||||
query
|
||||
512x512 256x256 224x224 varies varies
|
||||
Видов на
|
||||
локацию
|
||||
```
|
||||
24 (D) 54 (D) 4 (D) 1 1
|
||||
```
|
||||
Мульти-
|
||||
высотность
|
||||
Да
|
||||
```
|
||||
(100/125/150 м)
|
||||
```
|
||||
```
|
||||
Да (3 выс.) Да (4
|
||||
```
|
||||
```
|
||||
выс.)
|
||||
```
|
||||
Нет Нет
|
||||
Мульти-
|
||||
ракурсность
|
||||
```
|
||||
Да (8 азимутов) Да (18 аз.) Нет Нет Нет
|
||||
```
|
||||
One-to-N
|
||||
matching
|
||||
Да Нет Нет Да Нет
|
||||
Semi-positives Да Нет Нет Нет Нет
|
||||
```
|
||||
Cross-area split Да (по
|
||||
```
|
||||
```
|
||||
странам)
|
||||
```
|
||||
Нет Нет Да Нет
|
||||
Gallery strategy Grid + 50%
|
||||
overlap
|
||||
GPS-centered
|
||||
1:1
|
||||
GPS-
|
||||
centered
|
||||
1:1
|
||||
Grid,
|
||||
semi-
|
||||
pos.
|
||||
GPS-
|
||||
centered
|
||||
1:1
|
||||
```
|
||||
Gallery size (DB) 274K 951 200 90K+ 44K
|
||||
```
|
||||
Дрон-виды Синтетические
|
||||
```
|
||||
(GE)
|
||||
```
|
||||
Синтетические
|
||||
```
|
||||
(GE)
|
||||
```
|
||||
Реальные — —
|
||||
├── weights_e6_all.pth # 95 MB, 24.9M params
|
||||
└── README.md
|
||||
Характеристика World-UAV University-1652 SUES-200 VIGOR CVUSA
|
||||
```
|
||||
Публичный Да (CC BY-NC
|
||||
```
|
||||
```
|
||||
4.0)
|
||||
```
|
||||
Да Да Да Да
|
||||
12. ЗАМЕЧАНИЯ ПО КАЧЕСТВУ ДАННЫХ
|
||||
```
|
||||
12.1. Неполные сцены (17 из 171 Country)
|
||||
```
|
||||
Следующие сцены не содержат DB-кропов, GPS-данных и positive.json:
|
||||
```
|
||||
12.2. Ошибки именования папок (6+ сцен)
|
||||
```
|
||||
Сцена Ошибка Ожидание
|
||||
French/Lyons/PartDieu eight150_rot135 height150_rot135
|
||||
French/Marseilles/LaPlaine 125_rot315 height125_rot315
|
||||
Spain/Barcelona/SantMarti ght100_rot90 height100_rot90
|
||||
Japan/Tokyo/Shibuya height150_rot3155 height150_rot315
|
||||
Italy/Turin/Crocetta height150_rot22 height150_rot225
|
||||
```
|
||||
English/Birmingham/Harborne height150_rot0 (1) дубликат
|
||||
```
|
||||
12.3. Общие замечания
|
||||
```
|
||||
Edinburgh: все 5 (CastleHill, Dalry, Haymarket, NewTown, Stockbride)
|
||||
```
|
||||
```
|
||||
London: все 5 (CamdenTown, CoventGarden, Fitzrovia, Mayfair, SoHo)
|
||||
```
|
||||
```
|
||||
Manchester: все 5 (Ancoats, Castlefield, Deansgate, NorthernQuarter, Piccadilly)
|
||||
```
|
||||
```
|
||||
Birmingham: JewelleryQuarter
|
||||
```
|
||||
```
|
||||
Chicago: __MACOSX (артефакт macOS-архивации)
|
||||
```
|
||||
```
|
||||
Domain gap: дроновые виды синтетические (Google Earth 3D) — не реальные БПЛА
|
||||
```
|
||||
Нет сезонной вариативности: Google Earth рендерит одну "замороженную" версию
|
||||
сцены
|
||||
Нет погодных / световых вариаций: нет fog, rain, night
|
||||
Воспроизводимость: Google периодически обновляет 3D-модели, что может
|
||||
нарушить воспроизводимость
|
||||
```
|
||||
Географический bias: нет Африки (кроме Terrain), нет Юго-Восточной Азии, нет
|
||||
```
|
||||
арктических регионов в Country
|
||||
13. КЛЮЧЕВЫЕ ЧИСЛА ДЛЯ ЦИТИРОВАНИЯ
|
||||
Метрика Значение
|
||||
Всего сцен 372
|
||||
Всего изображений 927,427
|
||||
```
|
||||
Стран 11 (Country)
|
||||
```
|
||||
```
|
||||
Типов ландшафта 27 (Terrain)
|
||||
```
|
||||
Query-кадров 652,744
|
||||
DB-кропов 274,683
|
||||
Размер на диске ~181 GB
|
||||
Разрешение query 512x512 px
|
||||
FOV камеры 30°
|
||||
```
|
||||
Высоты 3 (100, 125, 150 м)
|
||||
```
|
||||
```
|
||||
Азимуты 8 (шаг 45°)
|
||||
```
|
||||
```
|
||||
Overlap нарезки 50% (stride = crop_size / 2)
|
||||
```
|
||||
```
|
||||
Crop sizes 11 уникальных (от 100 до 1000 px)
|
||||
```
|
||||
```
|
||||
Модель (params) 24.9M (GroupNet + DINOv2-ViT-S/14 + MulConvAP)
|
||||
```
|
||||
217
app.js
Normal file
217
app.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import * as Cesium from "cesium";
|
||||
|
||||
|
||||
Cesium.Ion.defaultAccessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI3NjcxNWZiYy1mZjFiLTQzNGEtYmEwNy1hMmU0ZDg4NWI2ZGUiLCJpZCI6Mzc3NjU2LCJpYXQiOjE3NjgyMTk2NzJ9.D5DL5zrDi4yK6OPgq-ypC4hM5hfqYKODvppgakc7y3g";
|
||||
|
||||
const creditContainer = document.createElement('div');
|
||||
creditContainer.style.display = 'none';
|
||||
|
||||
const viewer = new Cesium.Viewer("cesiumContainer", {
|
||||
timeline: false,
|
||||
animation: false,
|
||||
sceneModePicker: false,
|
||||
baseLayerPicker: false,
|
||||
geocoder: false,
|
||||
homeButton: false,
|
||||
navigationHelpButton: false,
|
||||
terrainProvider: await Cesium.createWorldTerrainAsync(),
|
||||
creditContainer: creditContainer,
|
||||
contextOptions: {
|
||||
webgl: {
|
||||
preserveDrawingBuffer: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const tileset = await Cesium.Cesium3DTileset.fromIonAssetId(2275207);
|
||||
viewer.scene.primitives.add(tileset);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
/*
|
||||
=================== Настройка отображения ===================
|
||||
*/
|
||||
|
||||
const outputWidth = 512;
|
||||
const outputHeight = 512;
|
||||
|
||||
viewer.scene.canvas.style.width = outputWidth + 'px';
|
||||
viewer.scene.canvas.style.height = outputHeight + 'px';
|
||||
viewer.container.style.width = outputWidth + 'px';
|
||||
viewer.container.style.height = outputHeight + 'px';
|
||||
|
||||
viewer.resize(); // важно: обновляет внутренние размеры рендерера
|
||||
|
||||
/*
|
||||
=================== Вспомагательные функции ===================
|
||||
*/
|
||||
|
||||
function dmsToDecimal(dms) {
|
||||
// Пример входа: "48°18'20\"N" или "16°24'41\"E"
|
||||
const regex = /(\d+)°(\d+)'(\d+)"?([NSEW])/i;
|
||||
const match = dms.match(regex);
|
||||
if (!match) {
|
||||
throw new Error("Неверный формат координат: " + dms);
|
||||
}
|
||||
|
||||
const degrees = parseInt(match[1], 10);
|
||||
const minutes = parseInt(match[2], 10);
|
||||
const seconds = parseInt(match[3], 10);
|
||||
const direction = match[4].toUpperCase();
|
||||
|
||||
let decimal = degrees + minutes / 60 + seconds / 3600;
|
||||
|
||||
// Южная и Западная широты — отрицательные
|
||||
if (direction === "S" || direction === "W") {
|
||||
decimal = -decimal;
|
||||
}
|
||||
|
||||
return decimal;
|
||||
}
|
||||
|
||||
function metersToLat(m) {
|
||||
return m / METERS_PER_DEG_LAT;
|
||||
}
|
||||
|
||||
function metersToLon(m, latDeg) {
|
||||
return m / (METERS_PER_DEG_LAT * Math.cos(Cesium.Math.toRadians(latDeg)));
|
||||
}
|
||||
|
||||
function saveImage(filename) {
|
||||
const canvas = viewer.scene.canvas;
|
||||
const image = canvas.toDataURL("image/png");
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = image;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
function downloadFile(filename, content, mime = "text/plain") {
|
||||
const blob = new Blob([content], { type: mime });
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/*
|
||||
=================== Константы и переменные ===================
|
||||
*/
|
||||
|
||||
const METERS_PER_DEG_LAT = 111_320;
|
||||
|
||||
// ---- Параметры сетки ----
|
||||
|
||||
const GRID = 2;
|
||||
const TILE_M = 50;
|
||||
const CAMERA_HEIGHT = 450;
|
||||
|
||||
// ---- Координаты ----
|
||||
|
||||
const latDMS = "45°25'3\"N";
|
||||
const lonDMS = "9°04'15\"E";
|
||||
//45°25'35"N 9°04'15"E
|
||||
|
||||
const half = (GRID - 1) * TILE_M / 2;
|
||||
|
||||
const CENTER_LAT = dmsToDecimal(latDMS);
|
||||
const CENTER_LON = dmsToDecimal(lonDMS);
|
||||
const startLat = CENTER_LAT + metersToLat(half);
|
||||
const startLon = CENTER_LON - metersToLon(half, CENTER_LAT);
|
||||
|
||||
|
||||
let row = 0;
|
||||
let col = 0;
|
||||
let index = 0;
|
||||
const offset = 0.0005;
|
||||
|
||||
/*
|
||||
=================== Главный цикл сбора ===================
|
||||
*/
|
||||
|
||||
var flag = false;
|
||||
|
||||
function captureNextTile() {
|
||||
|
||||
if (flag == false) {
|
||||
setTimeout(() => {
|
||||
flag = true;
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
const lat = startLat - metersToLat(row * TILE_M);
|
||||
const lon = startLon + metersToLon(col * TILE_M, lat);
|
||||
|
||||
downloadFile(
|
||||
`tile_${index}_sat.json`,
|
||||
JSON.stringify({ lat: lat, lon: lon }, null, 2),
|
||||
"application/json"
|
||||
);
|
||||
|
||||
// ---------- Спутник ----------
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(lon, lat, CAMERA_HEIGHT),
|
||||
orientation: {
|
||||
heading: Cesium.Math.toRadians(0),
|
||||
pitch: Cesium.Math.toRadians(-90),
|
||||
roll: 0
|
||||
}
|
||||
});
|
||||
|
||||
viewer.scene.requestRender();
|
||||
|
||||
setTimeout(() => {
|
||||
saveImage(`tile_${index}_sat.png`);
|
||||
|
||||
// ---------- Дрон ----------
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(lon, lat - offset, CAMERA_HEIGHT),
|
||||
orientation: {
|
||||
heading: Cesium.Math.toRadians(0),
|
||||
pitch: Cesium.Math.toRadians(-60),
|
||||
roll: 0
|
||||
}
|
||||
});
|
||||
|
||||
viewer.scene.requestRender();
|
||||
|
||||
setTimeout(() => {
|
||||
saveImage(`tile_${index}_drone.png`);
|
||||
|
||||
// ---- След. ячейка ----
|
||||
col++;
|
||||
if (col >= GRID) {
|
||||
col = 0;
|
||||
row++;
|
||||
}
|
||||
index++;
|
||||
|
||||
captureNextTile();
|
||||
}, 2500); // Задержка дрон-рендера
|
||||
|
||||
}, 2500); // Задержка спутник-рендера
|
||||
}
|
||||
|
||||
/*
|
||||
=================== Запуск ===================
|
||||
*/
|
||||
|
||||
viewer.scene.postRender.addEventListener(function once() {
|
||||
viewer.scene.postRender.removeEventListener(once);
|
||||
|
||||
setTimeout(() => {
|
||||
captureNextTile();
|
||||
}, 9000);
|
||||
});
|
||||
|
||||
908
capture.html
Normal file
908
capture.html
Normal file
@@ -0,0 +1,908 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Захват изображений</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #0f172a;
|
||||
color: #f1f5f9;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
#cesiumContainer {
|
||||
width: 512px;
|
||||
height: 512px;
|
||||
border: 2px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem 2rem;
|
||||
background: #1e293b;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #334155;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status h2 {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.progress {
|
||||
font-size: 1.5rem;
|
||||
color: #10b981;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.log {
|
||||
margin-top: 1rem;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
background: #1e293b;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 0.5rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
</style>
|
||||
<link href="https://cesium.com/downloads/cesiumjs/releases/1.121/Build/Cesium/Widgets/widgets.css" rel="stylesheet">
|
||||
<script src="https://cesium.com/downloads/cesiumjs/releases/1.121/Build/Cesium/Cesium.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Захват изображений</h1>
|
||||
</div>
|
||||
|
||||
<div id="cesiumContainer"></div>
|
||||
|
||||
<div id="mapPanel" style="position:fixed;top:12px;right:12px;width:300px;background:#1e293b;border:1px solid #334155;border-radius:8px;padding:8px;z-index:1000;box-shadow:0 10px 30px rgba(0,0,0,0.3);">
|
||||
<div id="mapLabel" style="font-size:0.85rem;color:#94a3b8;margin-bottom:6px;font-weight:500;">Карта зоны</div>
|
||||
<canvas id="mapCanvas" width="284" height="284" style="display:block;background:#0f172a;border-radius:4px;"></canvas>
|
||||
</div>
|
||||
|
||||
<div class="status">
|
||||
<h2>Статус</h2>
|
||||
<div class="progress" id="progress">Загрузка...</div>
|
||||
</div>
|
||||
|
||||
<div class="log" id="log"></div>
|
||||
|
||||
<script>
|
||||
const progressEl = document.getElementById('progress');
|
||||
const logEl = document.getElementById('log');
|
||||
|
||||
const METERS_PER_DEG_LAT = 111320;
|
||||
const DMS_VALUE_REGEX = /^\s*(\d+)\s*(?:°|В°)\s*(\d+)\s*'\s*([\d.]+)\s*"?\s*([NSEW])\s*$/i;
|
||||
const DMS_TOKEN_REGEX = /\d+\s*(?:°|В°)\s*\d+\s*'\s*[\d.]+\s*"?\s*[NSEW]/gi;
|
||||
|
||||
function log(message) {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'log-entry';
|
||||
const timestamp = new Date().toLocaleTimeString('ru-RU');
|
||||
entry.textContent = `[${timestamp}] ${message}`;
|
||||
logEl.appendChild(entry);
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function setProgress(text) {
|
||||
progressEl.textContent = text;
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
const response = await fetch('/api/get-settings');
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.settings) {
|
||||
throw new Error('Не удалось загрузить настройки');
|
||||
}
|
||||
return data.settings;
|
||||
}
|
||||
|
||||
function metersToLat(meters) {
|
||||
return meters / METERS_PER_DEG_LAT;
|
||||
}
|
||||
|
||||
function metersToLon(meters, latDeg) {
|
||||
return meters / (METERS_PER_DEG_LAT * Math.cos((latDeg * Math.PI) / 180));
|
||||
}
|
||||
|
||||
function dmsToDecimal(dms) {
|
||||
const match = String(dms).trim().match(DMS_VALUE_REGEX);
|
||||
if (!match) {
|
||||
throw new Error(`Неверный формат координат: ${dms}`);
|
||||
}
|
||||
|
||||
const degrees = Number.parseInt(match[1], 10);
|
||||
const minutes = Number.parseInt(match[2], 10);
|
||||
const seconds = Number.parseFloat(match[3]);
|
||||
const direction = match[4].toUpperCase();
|
||||
|
||||
let decimal = degrees + minutes / 60 + seconds / 3600;
|
||||
if (direction === 'S' || direction === 'W') {
|
||||
decimal = -decimal;
|
||||
}
|
||||
|
||||
return decimal;
|
||||
}
|
||||
|
||||
function parseCoordinatePair(value) {
|
||||
const tokens = String(value || '').match(DMS_TOKEN_REGEX) || [];
|
||||
if (tokens.length !== 2) {
|
||||
throw new Error(`Ожидалась пара DMS: ${value}`);
|
||||
}
|
||||
const coords = tokens.map((t) => ({
|
||||
value: dmsToDecimal(t),
|
||||
direction: t.trim().slice(-1).toUpperCase()
|
||||
}));
|
||||
const lat = coords.find((c) => c.direction === 'N' || c.direction === 'S');
|
||||
const lon = coords.find((c) => c.direction === 'E' || c.direction === 'W');
|
||||
if (!lat || !lon) {
|
||||
throw new Error(`В паре должны быть широта и долгота: ${value}`);
|
||||
}
|
||||
return { lat: lat.value, lon: lon.value };
|
||||
}
|
||||
|
||||
function getBoundsFromSettings(settings) {
|
||||
const p1 = parseCoordinatePair(settings.point1);
|
||||
const p2 = parseCoordinatePair(settings.point2);
|
||||
return {
|
||||
minLat: Math.min(p1.lat, p2.lat),
|
||||
maxLat: Math.max(p1.lat, p2.lat),
|
||||
minLon: Math.min(p1.lon, p2.lon),
|
||||
maxLon: Math.max(p1.lon, p2.lon)
|
||||
};
|
||||
}
|
||||
|
||||
function computeGrid(bounds, tileMeters) {
|
||||
const anchorLat = bounds.maxLat;
|
||||
const latSpanM = (bounds.maxLat - bounds.minLat) * METERS_PER_DEG_LAT;
|
||||
const lonSpanM = (bounds.maxLon - bounds.minLon) * METERS_PER_DEG_LAT * Math.cos((anchorLat * Math.PI) / 180);
|
||||
const cols = Math.max(1, Math.floor(lonSpanM / tileMeters) + 1);
|
||||
const rows = Math.max(1, Math.floor(latSpanM / tileMeters) + 1);
|
||||
return { cols, rows };
|
||||
}
|
||||
|
||||
function generateTiles(bounds, step, tileMeters) {
|
||||
const { cols, rows } = computeGrid(bounds, tileMeters);
|
||||
const tiles = [];
|
||||
let index = 1;
|
||||
|
||||
for (let row = 0; row < rows; row += step) {
|
||||
for (let col = 0; col < cols; col += step) {
|
||||
const lat = bounds.maxLat - metersToLat(row * tileMeters);
|
||||
const lon = bounds.minLon + metersToLon(col * tileMeters, lat);
|
||||
tiles.push({ index: index++, row, col, lat, lon });
|
||||
}
|
||||
}
|
||||
|
||||
return { tiles, gridRows: rows, gridCols: cols, tileSpanM: tileMeters };
|
||||
}
|
||||
|
||||
function captureCropped(viewer, cropSize, format = 'png', quality = 0.92) {
|
||||
const srcCanvas = viewer.scene.canvas;
|
||||
const { width, height } = srcCanvas;
|
||||
const size = Math.min(cropSize, width, height);
|
||||
const out = document.createElement('canvas');
|
||||
out.width = size;
|
||||
out.height = size;
|
||||
const sx = Math.floor((width - size) / 2);
|
||||
const sy = Math.floor((height - size) / 2);
|
||||
out.getContext('2d').drawImage(srcCanvas, sx, sy, size, size, 0, 0, size, size);
|
||||
const mime = format === 'jpeg' ? 'image/jpeg' : 'image/png';
|
||||
return out.toDataURL(mime, quality);
|
||||
}
|
||||
|
||||
async function detect3DContent(viewer, terrainProvider, tiles, tileSpanM) {
|
||||
const SAMPLE_GRID = 3;
|
||||
const BUILDING_THRESHOLD_M = 5;
|
||||
const MIN_POINTS_WITH_BUILDING = 1;
|
||||
|
||||
const sceneCarto = [];
|
||||
const terrainCarto = [];
|
||||
const offsetM = tileSpanM / 3;
|
||||
|
||||
tiles.forEach((tile) => {
|
||||
const latOffsetDeg = offsetM / 111320;
|
||||
const lonOffsetDeg = offsetM / (111320 * Math.cos((tile.lat * Math.PI) / 180));
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
const lon = tile.lon + dx * lonOffsetDeg;
|
||||
const lat = tile.lat + dy * latOffsetDeg;
|
||||
sceneCarto.push(Cesium.Cartographic.fromDegrees(lon, lat));
|
||||
terrainCarto.push(Cesium.Cartographic.fromDegrees(lon, lat));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
viewer.scene.sampleHeightMostDetailed(sceneCarto),
|
||||
Cesium.sampleTerrainMostDetailed(terrainProvider, terrainCarto)
|
||||
]);
|
||||
} catch (e) {
|
||||
log(`Ошибка при анализе 3D: ${e.message}`);
|
||||
tiles.forEach(t => { t.has3D = true; });
|
||||
return;
|
||||
}
|
||||
|
||||
const perTile = SAMPLE_GRID * SAMPLE_GRID;
|
||||
tiles.forEach((tile, i) => {
|
||||
let buildingPoints = 0;
|
||||
for (let j = 0; j < perTile; j++) {
|
||||
const sceneH = sceneCarto[i * perTile + j]?.height;
|
||||
const terrainH = terrainCarto[i * perTile + j]?.height;
|
||||
if (Number.isFinite(sceneH) && Number.isFinite(terrainH)) {
|
||||
if ((sceneH - terrainH) > BUILDING_THRESHOLD_M) {
|
||||
buildingPoints++;
|
||||
}
|
||||
}
|
||||
}
|
||||
tile.has3D = buildingPoints >= MIN_POINTS_WITH_BUILDING;
|
||||
});
|
||||
}
|
||||
|
||||
async function postJson(url, body) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || `Ошибка запроса ${url}`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function saveDbCrop(settings, tile, png, resX, resY, sizeTag) {
|
||||
return postJson('/api/save-db-crop', {
|
||||
...settings,
|
||||
col: tile.col,
|
||||
row: tile.row,
|
||||
lat: tile.lat,
|
||||
lon: tile.lon,
|
||||
res_x: resX,
|
||||
res_y: resY,
|
||||
size_tag: sizeTag,
|
||||
png
|
||||
});
|
||||
}
|
||||
|
||||
async function saveDroneFrame(settings, heightValue, rotValue, frameIdx, jpeg) {
|
||||
return postJson('/api/save-drone-frame', {
|
||||
...settings,
|
||||
height: heightValue,
|
||||
rot: rotValue,
|
||||
frameIdx,
|
||||
jpeg
|
||||
});
|
||||
}
|
||||
|
||||
async function saveTrajectory(settings, heightValue, rotValue, trajectory) {
|
||||
return postJson('/api/save-trajectory', {
|
||||
...settings,
|
||||
height: heightValue,
|
||||
rot: rotValue,
|
||||
trajectory
|
||||
});
|
||||
}
|
||||
|
||||
async function saveMergeTif(settings, png) {
|
||||
return postJson('/api/save-merge-tif', {
|
||||
...settings,
|
||||
png
|
||||
});
|
||||
}
|
||||
|
||||
async function saveAnnotations(settings, positive, semiPositive) {
|
||||
return postJson('/api/save-annotations', {
|
||||
...settings,
|
||||
positive,
|
||||
semi_positive: semiPositive
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForSceneReady(viewer, tileset, timeoutMs = 30000, minWaitMs = 1500) {
|
||||
return new Promise((resolve) => {
|
||||
const start = Date.now();
|
||||
let stableFrames = 0;
|
||||
let sawPending = false;
|
||||
const STABLE_NEEDED = 10;
|
||||
|
||||
function onPostRender() {
|
||||
const elapsed = Date.now() - start;
|
||||
const stats = tileset ? tileset.statistics : null;
|
||||
const pending = stats
|
||||
? (stats.numberOfPendingRequests + stats.numberOfTilesProcessing)
|
||||
: 0;
|
||||
|
||||
if (pending > 0) {
|
||||
sawPending = true;
|
||||
stableFrames = 0;
|
||||
} else if (elapsed >= minWaitMs) {
|
||||
stableFrames++;
|
||||
if (stableFrames >= STABLE_NEEDED) {
|
||||
viewer.scene.postRender.removeEventListener(onPostRender);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (elapsed > timeoutMs) {
|
||||
viewer.scene.postRender.removeEventListener(onPostRender);
|
||||
log(`Таймаут ожидания карты (pending=${pending}, sawPending=${sawPending})`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
viewer.scene.requestRender();
|
||||
}
|
||||
|
||||
viewer.scene.postRender.addEventListener(onPostRender);
|
||||
viewer.scene.requestRender();
|
||||
});
|
||||
}
|
||||
|
||||
async function createViewer(canvasSize) {
|
||||
Cesium.Ion.defaultAccessToken =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI3NjcxNWZiYy1mZjFiLTQzNGEtYmEwNy1hMmU0ZDg4NWI2ZGUiLCJpZCI6Mzc3NjU2LCJpYXQiOjE3NjgyMTk2NzJ9.D5DL5zrDi4yK6OPgq-ypC4hM5hfqYKODvppgakc7y3g';
|
||||
|
||||
const creditContainer = document.createElement('div');
|
||||
creditContainer.style.display = 'none';
|
||||
|
||||
const viewer = new Cesium.Viewer('cesiumContainer', {
|
||||
globe: false,
|
||||
skyAtmosphere: new Cesium.SkyAtmosphere(),
|
||||
sceneModePicker: false,
|
||||
baseLayerPicker: false,
|
||||
geocoder: Cesium.IonGeocodeProviderType?.GOOGLE || false,
|
||||
timeline: false,
|
||||
animation: false,
|
||||
creditContainer: creditContainer,
|
||||
contextOptions: {
|
||||
webgl: {
|
||||
preserveDrawingBuffer: true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const outputWidth = canvasSize;
|
||||
const outputHeight = canvasSize;
|
||||
viewer.scene.canvas.style.width = `${outputWidth}px`;
|
||||
viewer.scene.canvas.style.height = `${outputHeight}px`;
|
||||
viewer.container.style.width = `${outputWidth}px`;
|
||||
viewer.container.style.height = `${outputHeight}px`;
|
||||
const cesiumContainerEl = document.getElementById('cesiumContainer');
|
||||
cesiumContainerEl.style.width = `${outputWidth}px`;
|
||||
cesiumContainerEl.style.height = `${outputHeight}px`;
|
||||
viewer.resize();
|
||||
|
||||
let tileset;
|
||||
try {
|
||||
tileset = await Cesium.Cesium3DTileset.fromIonAssetId(2275207);
|
||||
viewer.scene.primitives.add(tileset);
|
||||
if (tileset.readyPromise) {
|
||||
await tileset.readyPromise;
|
||||
}
|
||||
log('Tileset загружен');
|
||||
} catch (error) {
|
||||
log(`Ошибка загрузки tileset: ${error.message}`);
|
||||
}
|
||||
|
||||
return { viewer, tileset };
|
||||
}
|
||||
|
||||
const mapCanvas = document.getElementById('mapCanvas');
|
||||
const mapCtx = mapCanvas.getContext('2d');
|
||||
const mapLabel = document.getElementById('mapLabel');
|
||||
const MAP_PADDING = 16;
|
||||
let mapState = null;
|
||||
const mapTileCache = new Map();
|
||||
let mapTileRedrawScheduled = false;
|
||||
|
||||
function mapViewport() {
|
||||
if (!mapState) return null;
|
||||
const { bounds } = mapState;
|
||||
const w = mapCanvas.width - MAP_PADDING * 2;
|
||||
const h = mapCanvas.height - MAP_PADDING * 2;
|
||||
const lonRange = Math.max(1e-9, bounds.maxLon - bounds.minLon);
|
||||
const latRange = Math.max(1e-9, bounds.maxLat - bounds.minLat);
|
||||
const midLat = (bounds.maxLat + bounds.minLat) / 2;
|
||||
const aspectGeo = (lonRange * Math.cos(midLat * Math.PI / 180)) / latRange;
|
||||
const aspectCanvas = w / h;
|
||||
let sw, sh;
|
||||
if (aspectGeo > aspectCanvas) { sw = w; sh = w / aspectGeo; }
|
||||
else { sh = h; sw = h * aspectGeo; }
|
||||
const ox = MAP_PADDING + (w - sw) / 2;
|
||||
const oy = MAP_PADDING + (h - sh) / 2;
|
||||
return { ox, oy, sw, sh, lonRange, latRange, bounds };
|
||||
}
|
||||
|
||||
function mapProject(lat, lon, vp) {
|
||||
return {
|
||||
x: vp.ox + ((lon - vp.bounds.minLon) / vp.lonRange) * vp.sw,
|
||||
y: vp.oy + ((vp.bounds.maxLat - lat) / vp.latRange) * vp.sh
|
||||
};
|
||||
}
|
||||
|
||||
function mapLonToX(lon, z) {
|
||||
const n = 2 ** z;
|
||||
return ((lon + 180) / 360) * n * 256;
|
||||
}
|
||||
|
||||
function mapLatToY(lat, z) {
|
||||
const n = 2 ** z;
|
||||
const rad = (lat * Math.PI) / 180;
|
||||
return ((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * n * 256;
|
||||
}
|
||||
|
||||
function mapChooseZoom(vp) {
|
||||
const lonRange = Math.max(1e-9, vp.bounds.maxLon - vp.bounds.minLon);
|
||||
const z = Math.floor(Math.log2((360 * vp.sw) / (lonRange * 256)));
|
||||
return Math.max(2, Math.min(18, Number.isFinite(z) ? z : 12));
|
||||
}
|
||||
|
||||
function mapGetTile(z, x, y) {
|
||||
const n = 2 ** z;
|
||||
if (y < 0 || y >= n) {
|
||||
return null;
|
||||
}
|
||||
const wrappedX = ((x % n) + n) % n;
|
||||
const key = `${z}/${wrappedX}/${y}`;
|
||||
if (mapTileCache.has(key)) {
|
||||
return mapTileCache.get(key);
|
||||
}
|
||||
|
||||
const tile = { status: 'loading', img: null };
|
||||
mapTileCache.set(key, tile);
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
tile.status = 'ready';
|
||||
tile.img = img;
|
||||
if (!mapTileRedrawScheduled) {
|
||||
mapTileRedrawScheduled = true;
|
||||
setTimeout(() => {
|
||||
mapTileRedrawScheduled = false;
|
||||
mapDraw();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
img.onerror = () => {
|
||||
tile.status = 'error';
|
||||
};
|
||||
img.src = `https://tile.openstreetmap.org/${z}/${wrappedX}/${y}.png`;
|
||||
|
||||
return tile;
|
||||
}
|
||||
|
||||
function mapDrawTiles(vp) {
|
||||
const z = mapChooseZoom(vp);
|
||||
const xMin = mapLonToX(vp.bounds.minLon, z);
|
||||
const xMax = mapLonToX(vp.bounds.maxLon, z);
|
||||
const yMin = mapLatToY(vp.bounds.maxLat, z);
|
||||
const yMax = mapLatToY(vp.bounds.minLat, z);
|
||||
|
||||
const mapW = Math.max(1, xMax - xMin);
|
||||
const mapH = Math.max(1, yMax - yMin);
|
||||
const scaleX = vp.sw / mapW;
|
||||
const scaleY = vp.sh / mapH;
|
||||
|
||||
const tileXStart = Math.floor(xMin / 256);
|
||||
const tileXEnd = Math.floor(xMax / 256);
|
||||
const tileYStart = Math.floor(yMin / 256);
|
||||
const tileYEnd = Math.floor(yMax / 256);
|
||||
|
||||
for (let ty = tileYStart; ty <= tileYEnd; ty += 1) {
|
||||
for (let tx = tileXStart; tx <= tileXEnd; tx += 1) {
|
||||
const tile = mapGetTile(z, tx, ty);
|
||||
if (!tile || tile.status !== 'ready' || !tile.img) continue;
|
||||
|
||||
const tilePxX = tx * 256;
|
||||
const tilePxY = ty * 256;
|
||||
const dx = vp.ox + (tilePxX - xMin) * scaleX;
|
||||
const dy = vp.oy + (tilePxY - yMin) * scaleY;
|
||||
const dw = 256 * scaleX;
|
||||
const dh = 256 * scaleY;
|
||||
mapCtx.drawImage(tile.img, dx, dy, dw, dh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapDraw() {
|
||||
mapCtx.fillStyle = '#0f172a';
|
||||
mapCtx.fillRect(0, 0, mapCanvas.width, mapCanvas.height);
|
||||
if (!mapState) return;
|
||||
const vp = mapViewport();
|
||||
if (!vp) return;
|
||||
mapCtx.fillStyle = 'rgba(15,23,42,0.65)';
|
||||
mapCtx.fillRect(vp.ox, vp.oy, vp.sw, vp.sh);
|
||||
mapDrawTiles(vp);
|
||||
mapCtx.strokeStyle = '#ef4444';
|
||||
mapCtx.lineWidth = 2;
|
||||
mapCtx.strokeRect(vp.ox, vp.oy, vp.sw, vp.sh);
|
||||
mapCtx.fillStyle = '#38bdf8';
|
||||
mapState.tiles.forEach((t) => {
|
||||
const p = mapProject(t.lat, t.lon, vp);
|
||||
mapCtx.beginPath();
|
||||
mapCtx.arc(p.x, p.y, 2, 0, Math.PI * 2);
|
||||
mapCtx.fill();
|
||||
});
|
||||
mapCtx.fillStyle = '#22c55e';
|
||||
mapState.visited.forEach((v) => {
|
||||
const p = mapProject(v.lat, v.lon, vp);
|
||||
mapCtx.beginPath();
|
||||
mapCtx.arc(p.x, p.y, 3, 0, Math.PI * 2);
|
||||
mapCtx.fill();
|
||||
});
|
||||
if (mapState.active) {
|
||||
const p = mapProject(mapState.active.lat, mapState.active.lon, vp);
|
||||
mapCtx.fillStyle = mapState.done ? '#10b981' : '#fbbf24';
|
||||
mapCtx.strokeStyle = '#0f172a';
|
||||
mapCtx.lineWidth = 2;
|
||||
mapCtx.beginPath();
|
||||
mapCtx.arc(p.x, p.y, 6, 0, Math.PI * 2);
|
||||
mapCtx.fill();
|
||||
mapCtx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function mapSetInit(bounds, tiles) {
|
||||
mapState = {
|
||||
bounds,
|
||||
tiles: tiles.map((t) => ({ lat: t.lat, lon: t.lon })),
|
||||
visited: [],
|
||||
active: null,
|
||||
done: false
|
||||
};
|
||||
mapLabel.textContent = `Карта зоны — ${tiles.length} точек`;
|
||||
mapDraw();
|
||||
}
|
||||
|
||||
function mapUpdate(idx, total, lat, lon) {
|
||||
if (!mapState) return;
|
||||
if (mapState.active) mapState.visited.push(mapState.active);
|
||||
mapState.active = { lat, lon };
|
||||
mapLabel.textContent = `Карта зоны — ${idx}/${total}`;
|
||||
mapDraw();
|
||||
}
|
||||
|
||||
function mapDone() {
|
||||
if (!mapState) return;
|
||||
mapState.done = true;
|
||||
mapLabel.textContent = `Карта зоны — готово`;
|
||||
mapDraw();
|
||||
}
|
||||
|
||||
mapDraw();
|
||||
|
||||
async function main() {
|
||||
setProgress('Загрузка настроек...');
|
||||
log('Загрузка настроек...');
|
||||
const settings = await loadSettings();
|
||||
|
||||
const sceneMeta = {
|
||||
dataset: settings.dataset || 'Country',
|
||||
country: settings.country || 'Custom',
|
||||
city: settings.city || 'Scene',
|
||||
region: settings.region || 'Region',
|
||||
point1: settings.point1 || '',
|
||||
point2: settings.point2 || '',
|
||||
tile_m: settings.tile_m
|
||||
};
|
||||
|
||||
const step = Math.max(1, Number.parseInt(settings.grid_step, 10) || 1);
|
||||
const tileMeters = Math.max(1, Number(settings.tile_m) || 1);
|
||||
const heightAbsolute = settings.height_absolute !== undefined ? Boolean(settings.height_absolute) : true;
|
||||
const droneSize = Math.max(1, Number.parseInt(settings.drone_size, 10) || 512);
|
||||
const dbCropSizes = Array.from(new Set(
|
||||
String(settings.db_crop_sizes || "")
|
||||
.split(",")
|
||||
.map((v) => Number.parseInt(v.trim(), 10))
|
||||
.filter((v) => Number.isInteger(v) && v > 0)
|
||||
));
|
||||
if (dbCropSizes.length === 0) {
|
||||
dbCropSizes.push(512);
|
||||
}
|
||||
const queryFovDeg = Math.min(179, Math.max(1, Number(settings.query_fov_deg) || 30));
|
||||
const queryPitchDeg = -90;
|
||||
const preloadEnabled = settings.preload !== undefined ? Boolean(settings.preload) : true;
|
||||
const skipEmpty = Boolean(settings.skip_empty);
|
||||
const offset = 0.0005;
|
||||
const ROT_VALUE = 0;
|
||||
|
||||
const enabledHeights = [];
|
||||
if (settings.height_100) enabledHeights.push(Math.max(1, Number(settings.height_100_value) || 100));
|
||||
if (settings.height_125) enabledHeights.push(Math.max(1, Number(settings.height_125_value) || 125));
|
||||
if (settings.height_150) enabledHeights.push(Math.max(1, Number(settings.height_150_value) || 150));
|
||||
if (enabledHeights.length === 0) {
|
||||
throw new Error('Не выбрана ни одна высота (100/125/150)');
|
||||
}
|
||||
|
||||
const bounds = getBoundsFromSettings(settings);
|
||||
const { tiles, gridRows, gridCols, tileSpanM } = generateTiles(bounds, step, tileMeters);
|
||||
|
||||
mapSetInit(bounds, tiles);
|
||||
|
||||
const totalShots = tiles.length * enabledHeights.length;
|
||||
log(`Сетка: ${gridCols}x${gridRows}, точек: ${tiles.length}, высот: ${enabledHeights.length}, кадров: ${totalShots}`);
|
||||
log(`Размеры DB: ${dbCropSizes.join(", ")} px | Query: ${droneSize}px`);
|
||||
log(`Параметры Query: FOV ${queryFovDeg}°, pitch ${queryPitchDeg}° (top-down)`);
|
||||
setProgress(`Подготовка ${totalShots} кадров...`);
|
||||
|
||||
if (typeof Cesium === 'undefined') {
|
||||
throw new Error('Карта не загружена');
|
||||
}
|
||||
|
||||
const maxDbCropSize = Math.max(...dbCropSizes);
|
||||
const canvasSize = Math.max(maxDbCropSize, droneSize);
|
||||
const { viewer, tileset } = await createViewer(canvasSize);
|
||||
viewer.camera.frustum.fovy = Cesium.Math.toRadians(queryFovDeg);
|
||||
|
||||
const terrainProvider = await Cesium.createWorldTerrainAsync();
|
||||
if (heightAbsolute) {
|
||||
tiles.forEach((tile) => {
|
||||
tile.cameraAlts = enabledHeights.slice();
|
||||
});
|
||||
log(`Высоты камеры: ${enabledHeights.join(', ')}м абсолютные`);
|
||||
} else {
|
||||
log('Определение высот рельефа...');
|
||||
const cartographics = tiles.map(t => Cesium.Cartographic.fromDegrees(t.lon, t.lat));
|
||||
const sampledPositions = await Cesium.sampleTerrainMostDetailed(terrainProvider, cartographics);
|
||||
tiles.forEach((tile, i) => {
|
||||
tile.groundHeight = sampledPositions[i].height || 0;
|
||||
tile.cameraAlts = enabledHeights.map(h => tile.groundHeight + h);
|
||||
});
|
||||
const minH = Math.round(Math.min(...tiles.map(t => t.groundHeight)));
|
||||
const maxH = Math.round(Math.max(...tiles.map(t => t.groundHeight)));
|
||||
log(`Рельеф: мин ${minH}м, макс ${maxH}м. Высоты: ${enabledHeights.join(', ')}м над рельефом`);
|
||||
}
|
||||
|
||||
if (skipEmpty) {
|
||||
log('Анализ наличия 3D-зданий в зонах...');
|
||||
await detect3DContent(viewer, terrainProvider, tiles, tileSpanM);
|
||||
const skipped = tiles.filter(t => !t.has3D).length;
|
||||
log(`Зон со зданиями: ${tiles.length - skipped}, без зданий (пропуск): ${skipped}`);
|
||||
} else {
|
||||
tiles.forEach(t => { t.has3D = true; });
|
||||
}
|
||||
|
||||
viewer.scene.postRender.addEventListener(async function once() {
|
||||
viewer.scene.postRender.removeEventListener(once);
|
||||
|
||||
log('Сохранение merge.tif...');
|
||||
const centerLat = (bounds.minLat + bounds.maxLat) / 2;
|
||||
const centerLon = (bounds.minLon + bounds.maxLon) / 2;
|
||||
const latSpanM = (bounds.maxLat - bounds.minLat) * METERS_PER_DEG_LAT;
|
||||
const lonSpanM = (bounds.maxLon - bounds.minLon) * METERS_PER_DEG_LAT * Math.cos((centerLat * Math.PI) / 180);
|
||||
const mergeSpanM = Math.max(latSpanM, lonSpanM);
|
||||
const mergeFovDeg = 60;
|
||||
const mergeAlt = Math.max(300, (mergeSpanM / 2) / Math.tan((mergeFovDeg * Math.PI / 180) / 2));
|
||||
viewer.camera.frustum.fovy = Cesium.Math.toRadians(mergeFovDeg);
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(centerLon, centerLat, mergeAlt),
|
||||
orientation: {
|
||||
heading: 0,
|
||||
pitch: Cesium.Math.toRadians(-90),
|
||||
roll: 0
|
||||
}
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
await waitForSceneReady(viewer, tileset, 20000, 1500);
|
||||
const mergePng = captureCropped(viewer, canvasSize, 'png');
|
||||
await saveMergeTif(sceneMeta, mergePng);
|
||||
viewer.camera.frustum.fovy = Cesium.Math.toRadians(queryFovDeg);
|
||||
|
||||
log('Разогрев tileset...');
|
||||
const avgBaseAlt = tiles.reduce((sum, t) => sum + t.cameraAlts[0], 0) / tiles.length;
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(centerLon, centerLat, avgBaseAlt + 500),
|
||||
orientation: {
|
||||
heading: 0,
|
||||
pitch: Cesium.Math.toRadians(-90),
|
||||
roll: 0
|
||||
}
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
await waitForSceneReady(viewer, tileset, 30000, 2500);
|
||||
|
||||
if (preloadEnabled) {
|
||||
log('Этап 1/2: предзагрузка тайлов...');
|
||||
let preIdx = 0;
|
||||
for (let i = 0; i < tiles.length; i += 1) {
|
||||
const tile = tiles[i];
|
||||
if (!tile.has3D) continue;
|
||||
for (let h = 0; h < enabledHeights.length; h += 1) {
|
||||
preIdx += 1;
|
||||
setProgress(`Прогрузка ${preIdx} / ${totalShots}`);
|
||||
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(tile.lon, tile.lat, tile.cameraAlts[h]),
|
||||
orientation: {
|
||||
heading: 0,
|
||||
pitch: Cesium.Math.toRadians(-90),
|
||||
roll: 0
|
||||
}
|
||||
});
|
||||
|
||||
viewer.scene.requestRender();
|
||||
await waitForSceneReady(viewer, tileset, 20000);
|
||||
}
|
||||
}
|
||||
log('Этап 2/2: сохранение скриншотов...');
|
||||
} else {
|
||||
log('Предзагрузка отключена, сразу сохраняем скриншоты...');
|
||||
}
|
||||
|
||||
const cameraFramesByHeight = {};
|
||||
enabledHeights.forEach((h) => { cameraFramesByHeight[h] = []; });
|
||||
const positiveAnnotations = {};
|
||||
const semiPositiveAnnotations = {};
|
||||
const availableTileSet = new Set(
|
||||
tiles.filter((t) => t.has3D).map((t) => `${t.col}:${t.row}`)
|
||||
);
|
||||
const preferredDbSize = dbCropSizes.includes(512) ? 512 : dbCropSizes[0];
|
||||
|
||||
const fovVerticalDeg = Cesium.Math.toDegrees(viewer.camera.frustum.fovy);
|
||||
|
||||
let shotIdx = 0;
|
||||
for (let i = 0; i < tiles.length; i += 1) {
|
||||
const tile = tiles[i];
|
||||
if (!tile.has3D) {
|
||||
log(`Точка ${tile.index}: 2D-зона, пропуск`);
|
||||
shotIdx += enabledHeights.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
mapUpdate(tile.index, tiles.length, tile.lat, tile.lon);
|
||||
|
||||
const dbAlt = tile.cameraAlts[0];
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(tile.lon, tile.lat, dbAlt),
|
||||
orientation: {
|
||||
heading: 0,
|
||||
pitch: Cesium.Math.toRadians(-90),
|
||||
roll: 0
|
||||
}
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
await waitForSceneReady(viewer, tileset, 20000);
|
||||
for (let s = 0; s < dbCropSizes.length; s += 1) {
|
||||
const dbSize = dbCropSizes[s];
|
||||
const satPng = captureCropped(viewer, dbSize, 'png');
|
||||
await saveDbCrop(sceneMeta, tile, satPng, dbSize, dbSize, dbSize);
|
||||
}
|
||||
|
||||
for (let h = 0; h < enabledHeights.length; h += 1) {
|
||||
shotIdx += 1;
|
||||
const heightValue = enabledHeights[h];
|
||||
const cameraAlt = tile.cameraAlts[h];
|
||||
setProgress(`Сохранение ${shotIdx} / ${totalShots}`);
|
||||
|
||||
const droneLat = tile.lat - offset;
|
||||
const droneLon = tile.lon;
|
||||
const dronePitchDeg = queryPitchDeg;
|
||||
const droneHeadingDeg = 0;
|
||||
|
||||
viewer.camera.setView({
|
||||
destination: Cesium.Cartesian3.fromDegrees(droneLon, droneLat, cameraAlt),
|
||||
orientation: {
|
||||
heading: Cesium.Math.toRadians(droneHeadingDeg),
|
||||
pitch: Cesium.Math.toRadians(dronePitchDeg),
|
||||
roll: 0
|
||||
}
|
||||
});
|
||||
viewer.scene.requestRender();
|
||||
await waitForSceneReady(viewer, tileset, 20000);
|
||||
const droneJpeg = captureCropped(viewer, droneSize, 'jpeg');
|
||||
await saveDroneFrame(sceneMeta, heightValue, ROT_VALUE, i, droneJpeg);
|
||||
|
||||
const variantName = `height${heightValue}_rot${ROT_VALUE}`;
|
||||
const frameKey = `${variantName}_${String(i).padStart(2, "0")}`;
|
||||
const positiveFilename = `crop_${tile.col}_${tile.row}_${preferredDbSize}.png`;
|
||||
positiveAnnotations[frameKey] = [positiveFilename];
|
||||
|
||||
const neighbors = [
|
||||
[tile.col - 1, tile.row],
|
||||
[tile.col + 1, tile.row],
|
||||
[tile.col, tile.row - 1],
|
||||
[tile.col, tile.row + 1]
|
||||
];
|
||||
const semi = neighbors
|
||||
.filter(([c, r]) => availableTileSet.has(`${c}:${r}`))
|
||||
.map(([c, r]) => `crop_${c}_${r}_${preferredDbSize}.png`);
|
||||
semiPositiveAnnotations[frameKey] = semi;
|
||||
|
||||
const ecef = Cesium.Cartesian3.fromDegrees(droneLon, droneLat, cameraAlt);
|
||||
cameraFramesByHeight[heightValue].push({
|
||||
position: { x: ecef.x, y: ecef.y, z: ecef.z },
|
||||
rotation: { x: dronePitchDeg, y: droneHeadingDeg, z: 0 },
|
||||
coordinate: {
|
||||
latitude: droneLat,
|
||||
longitude: droneLon,
|
||||
altitude: cameraAlt
|
||||
},
|
||||
fovVertical: fovVerticalDeg
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
log('Сохранение траекторий...');
|
||||
const numFrames = cameraFramesByHeight[enabledHeights[0]].length;
|
||||
const frameRate = 30;
|
||||
for (let h = 0; h < enabledHeights.length; h += 1) {
|
||||
const heightValue = enabledHeights[h];
|
||||
const trajectory = {
|
||||
name: `height${heightValue}_rot${ROT_VALUE}`,
|
||||
width: droneSize,
|
||||
height: droneSize,
|
||||
frameRate,
|
||||
numFrames,
|
||||
durationSeconds: numFrames / frameRate,
|
||||
cameraFrames: cameraFramesByHeight[heightValue]
|
||||
};
|
||||
await saveTrajectory(sceneMeta, heightValue, ROT_VALUE, trajectory);
|
||||
log(`Траектория height${heightValue}_rot${ROT_VALUE}.json сохранена`);
|
||||
}
|
||||
|
||||
log('Сохранение аннотаций positive/semi_positive...');
|
||||
await saveAnnotations(sceneMeta, positiveAnnotations, semiPositiveAnnotations);
|
||||
|
||||
mapDone();
|
||||
|
||||
setProgress('Готово');
|
||||
log(`Готово: захват завершен (${totalShots} кадров)`);
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
main().catch((error) => {
|
||||
setProgress('Ошибка');
|
||||
log(`Ошибка: ${error.message}`);
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
86
graphify-out/GRAPH_REPORT.md
Normal file
86
graphify-out/GRAPH_REPORT.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Graph Report - d:/Int SandCastle (2026-04-15)
|
||||
|
||||
## Corpus Check
|
||||
- Large corpus: 1277 files · ~9,282,356 words. Semantic extraction will be expensive (many Claude tokens). Consider running on a subfolder, or use --no-semantic to run AST-only.
|
||||
|
||||
## Summary
|
||||
- 77 nodes · 113 edges · 6 communities detected
|
||||
- Extraction: 94% EXTRACTED · 6% INFERRED · 0% AMBIGUOUS · INFERRED: 7 edges (avg confidence: 0.84)
|
||||
- Token cost: 0 input · 0 output
|
||||
|
||||
## Community Hubs (Navigation)
|
||||
- [[_COMMUNITY_REST API + Control Panel|REST API + Control Panel]]
|
||||
- [[_COMMUNITY_UI Controller (ui.js)|UI Controller (ui.js)]]
|
||||
- [[_COMMUNITY_Cesium Capture Pipeline|Cesium Capture Pipeline]]
|
||||
- [[_COMMUNITY_Coordinate Math Helpers|Coordinate Math Helpers]]
|
||||
- [[_COMMUNITY_Legacy app.js|Legacy app.js]]
|
||||
- [[_COMMUNITY_UAV-GeoLoc Dataset Tree|UAV-GeoLoc Dataset Tree]]
|
||||
|
||||
## God Nodes (most connected - your core abstractions)
|
||||
1. `SandCastleUI` - 15 edges
|
||||
2. `Control Panel (index.html)` - 9 edges
|
||||
3. `server.js (Express backend)` - 9 edges
|
||||
4. `generateCoordinates()` - 6 edges
|
||||
5. `output/ directory tree` - 5 edges
|
||||
6. `cameraFrames` - 5 edges
|
||||
7. `captureNextTile()` - 4 edges
|
||||
8. `getBoundsFromSettings()` - 4 edges
|
||||
9. `Cesium` - 4 edges
|
||||
10. `Camera trajectory JSON` - 4 edges
|
||||
|
||||
## Surprising Connections (you probably didn't know these)
|
||||
- `server.js (Express backend)` --exposes--> `POST /api/save-db-crop` [EXTRACTED]
|
||||
README.md → README.md _Bridges community 0 → community 5_
|
||||
- `server.js (Express backend)` --exposes--> `POST /api/save-trajectory` [EXTRACTED]
|
||||
README.md → README.md _Bridges community 0 → community 2_
|
||||
|
||||
## Hyperedges (group relationships)
|
||||
- **Capture pipeline** — readme_control_panel, readme_capture_html, readme_mapview_html, readme_broadcastchannel, readme_server_js [EXTRACTED 1.00]
|
||||
- **Trajectory schema fields** — readme_camera_frames, readme_ecef_position, readme_rotation, readme_coordinate, readme_fov_vertical [EXTRACTED 1.00]
|
||||
- **UAV-GeoLoc output layout** — readme_output_tree, readme_db_crop, readme_db_position_txt, readme_drone_frame, readme_trajectory, readme_region_txt [EXTRACTED 1.00]
|
||||
|
||||
## Communities
|
||||
|
||||
### Community 0 - "REST API + Control Panel"
|
||||
Cohesion: 0.13
|
||||
Nodes (18): POST /api/create-folders, GET /api/get-settings, POST /api/save-settings, POST /api/start-capture, Control Panel (index.html), DMS coordinates, Express, Grid step (meters) (+10 more)
|
||||
|
||||
### Community 1 - "UI Controller (ui.js)"
|
||||
Cohesion: 0.28
|
||||
Nodes (1): SandCastleUI
|
||||
|
||||
### Community 2 - "Cesium Capture Pipeline"
|
||||
Cohesion: 0.15
|
||||
Nodes (16): POST /api/save-trajectory, BroadcastChannel('sandcastle-map'), cameraFrames, capture.html (Cesium Viewer), Cesium, Cesium Ion Token (asset 2275207), coordinate (geodetic), ECEF position (Cartesian3.fromDegrees) (+8 more)
|
||||
|
||||
### Community 3 - "Coordinate Math Helpers"
|
||||
Cohesion: 0.26
|
||||
Nodes (10): computeGrid(), decimalToDMS(), generateCoordinates(), getBoundsFromSettings(), getScenePath(), metersToLat(), metersToLon(), normalizeSettings() (+2 more)
|
||||
|
||||
### Community 4 - "Legacy app.js"
|
||||
Cohesion: 0.43
|
||||
Nodes (4): captureNextTile(), downloadFile(), metersToLat(), metersToLon()
|
||||
|
||||
### Community 5 - "UAV-GeoLoc Dataset Tree"
|
||||
Cohesion: 0.33
|
||||
Nodes (7): POST /api/save-db-crop, POST /api/save-drone-frame, Dataset/Country/City/Region tree, DB crop (ortho crop), db_postion.txt, Drone frame (query), output/ directory tree
|
||||
|
||||
## Knowledge Gaps
|
||||
- **16 isolated node(s):** `Cesium Ion Token (asset 2275207)`, `ui.js (UI controller)`, `package.json`, `db_postion.txt`, `{region}.txt (bounds)` (+11 more)
|
||||
These have ≤1 connection - possible missing edges or undocumented components.
|
||||
|
||||
## Suggested Questions
|
||||
_Questions this graph is uniquely positioned to answer:_
|
||||
|
||||
- **Why does `Camera trajectory JSON` connect `Cesium Capture Pipeline` to `UAV-GeoLoc Dataset Tree`?**
|
||||
_High betweenness centrality (0.131) - this node is a cross-community bridge._
|
||||
- **Why does `server.js (Express backend)` connect `REST API + Control Panel` to `Cesium Capture Pipeline`, `UAV-GeoLoc Dataset Tree`?**
|
||||
_High betweenness centrality (0.110) - this node is a cross-community bridge._
|
||||
- **Why does `output/ directory tree` connect `UAV-GeoLoc Dataset Tree` to `REST API + Control Panel`, `Cesium Capture Pipeline`?**
|
||||
_High betweenness centrality (0.086) - this node is a cross-community bridge._
|
||||
- **Are the 4 inferred relationships involving `Control Panel (index.html)` (e.g. with `ui.js (UI controller)` and `POST /api/save-settings`) actually correct?**
|
||||
_`Control Panel (index.html)` has 4 INFERRED edges - model-reasoned connections that need verification._
|
||||
- **What connects `Cesium Ion Token (asset 2275207)`, `ui.js (UI controller)`, `package.json` to the rest of the system?**
|
||||
_16 weakly-connected nodes found - possible documentation gaps or missing edges._
|
||||
- **Should `REST API + Control Panel` be split into smaller, more focused modules?**
|
||||
_Cohesion score 0.13 - nodes in this community are weakly interconnected._
|
||||
1
graphify-out/cache/22c703cdee528e9e63d53a961b656d6da97a788db146a4f6f81e57e9cc29ab68.json
vendored
Normal file
1
graphify-out/cache/22c703cdee528e9e63d53a961b656d6da97a788db146a4f6f81e57e9cc29ab68.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
graphify-out/cache/65ff875203817b898ff241da68adf25c352f2c6709ad3bf1285f09bc3a5648f8.json
vendored
Normal file
1
graphify-out/cache/65ff875203817b898ff241da68adf25c352f2c6709ad3bf1285f09bc3a5648f8.json
vendored
Normal file
File diff suppressed because one or more lines are too long
1
graphify-out/cache/d9926deab971545bafef0edc95312d0cde26e4f5b7486db698de1332c03510de.json
vendored
Normal file
1
graphify-out/cache/d9926deab971545bafef0edc95312d0cde26e4f5b7486db698de1332c03510de.json
vendored
Normal file
File diff suppressed because one or more lines are too long
12
graphify-out/cost.json
Normal file
12
graphify-out/cost.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"runs": [
|
||||
{
|
||||
"date": "2026-04-15T06:49:58.200070+00:00",
|
||||
"input_tokens": 0,
|
||||
"output_tokens": 0,
|
||||
"files": 5
|
||||
}
|
||||
],
|
||||
"total_input_tokens": 0,
|
||||
"total_output_tokens": 0
|
||||
}
|
||||
257
graphify-out/graph.html
Normal file
257
graphify-out/graph.html
Normal file
File diff suppressed because one or more lines are too long
2319
graphify-out/graph.json
Normal file
2319
graphify-out/graph.json
Normal file
File diff suppressed because it is too large
Load Diff
7
graphify-out/manifest.json
Normal file
7
graphify-out/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"app.js": 1775217358.0,
|
||||
"server.js": 1776149190.0,
|
||||
"ui.js": 1776234151.967281,
|
||||
"README.md": 1776234555.7837043,
|
||||
"UAV-GeoLoc_Dataset_Analysis.pdf": 1776148726.9799173
|
||||
}
|
||||
224
index.html
Normal file
224
index.html
Normal file
@@ -0,0 +1,224 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Настройка координат</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Панель управления</h1>
|
||||
<p class="subtitle">Управление координатами и захватом изображений</p>
|
||||
</header>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- Форма настроек -->
|
||||
<div class="card settings-card">
|
||||
<h2>Настройки</h2>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="dataset">Dataset</label>
|
||||
<select id="dataset">
|
||||
<option value="Country">Country</option>
|
||||
<option value="Terrain">Terrain</option>
|
||||
<option value="Rot">Rot</option>
|
||||
</select>
|
||||
<span class="hint">Верхний уровень дерева</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="country">Country / Terrain type</label>
|
||||
<input type="text" id="country" placeholder="German" value="German">
|
||||
<span class="hint">2-й уровень</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="city">City / Scene</label>
|
||||
<input type="text" id="city" placeholder="Hannover" value="Hannover">
|
||||
<span class="hint">3-й уровень</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="region">Region</label>
|
||||
<input type="text" id="region" placeholder="Custom" value="Custom">
|
||||
<span class="hint">4-й уровень (имя сцены)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="point1">Точка 1 (широта и долгота, DMS)</label>
|
||||
<input type="text" id="point1" placeholder='52°29\'26.73"N 9°29\'47.96"E' value='52°29\'26.73"N 9°29\'47.96"E'>
|
||||
<span class="hint">Первый угол прямоугольника</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="point2">Точка 2 (широта и долгота, DMS)</label>
|
||||
<input type="text" id="point2" placeholder='52°17\'31.30"N 10°01\'32.89"E' value='52°17\'31.30"N 10°01\'32.89"E'>
|
||||
<span class="hint">Противоположный угол прямоугольника</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="tile_m">Шаг сетки (метры)</label>
|
||||
<input type="number" id="tile_m" min="1" max="1000" value="49">
|
||||
<span class="hint">Размер одного тайла</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="grid_step">Прореживание</label>
|
||||
<input type="number" id="grid_step" min="1" value="1">
|
||||
<span class="hint">1 = все точки, 2 = каждая вторая</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-block">
|
||||
<h3>DB (gallery / satellite)</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="db_crop_sizes">DB crop sizes (11 уникальных, через запятую)</label>
|
||||
<input type="text" id="db_crop_sizes" value="100,150,200,250,300,400,500,512,600,800,1000">
|
||||
<span class="hint">Например: 100,200,...; каждый размер сохранится отдельным PNG</span>
|
||||
<button type="button" id="fillDbSizesBtn" class="btn btn-secondary btn-small">Подставить стандартные 11 размеров</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-block">
|
||||
<h3>Query (drone)</h3>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="drone_size">Разрешение query кадра (px)</label>
|
||||
<input type="number" id="drone_size" min="1" max="2048" value="512">
|
||||
<span class="hint">Итоговый размер JPEG</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="query_fov_deg">FOV вертикальный (градусы)</label>
|
||||
<input type="number" id="query_fov_deg" min="1" max="179" step="0.1" value="30">
|
||||
<span class="hint">Влияет на угол обзора камеры query</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="query_pitch_deg">Pitch query камеры (градусы)</label>
|
||||
<input type="number" id="query_pitch_deg" min="-90" max="-90" step="0.1" value="-90" readonly>
|
||||
<span class="hint">Фиксировано: -90 (top-down)</span>
|
||||
</div>
|
||||
<div class="form-group"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-block">
|
||||
<h3>Высоты (query)</h3>
|
||||
<div class="height-grid">
|
||||
<label class="height-item">
|
||||
<input type="checkbox" id="height_100" checked>
|
||||
<span>H1</span>
|
||||
<input type="number" id="height_100_value" min="1" max="10000" value="100">
|
||||
</label>
|
||||
<label class="height-item">
|
||||
<input type="checkbox" id="height_125" checked>
|
||||
<span>H2</span>
|
||||
<input type="number" id="height_125_value" min="1" max="10000" value="125">
|
||||
</label>
|
||||
<label class="height-item">
|
||||
<input type="checkbox" id="height_150" checked>
|
||||
<span>H3</span>
|
||||
<input type="number" id="height_150_value" min="1" max="10000" value="150">
|
||||
</label>
|
||||
</div>
|
||||
<span class="hint">Чекбокс включает/выключает высоту, значение рядом редактируется вручную</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" id="height_absolute" style="width: auto;" checked>
|
||||
<span>Абсолютная высота</span>
|
||||
</label>
|
||||
<span class="hint">Выкл = высота над рельефом</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" id="preload" style="width: auto;" checked>
|
||||
<span>Предзагрузка тайлов</span>
|
||||
</label>
|
||||
<span class="hint">Выкл = скрины сразу без прогрева</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label style="display: flex; align-items: center; gap: 0.5rem;">
|
||||
<input type="checkbox" id="skip_empty" style="width: auto;">
|
||||
<span>Пропускать пустые зоны</span>
|
||||
</label>
|
||||
<span class="hint">Не сохранять области без 3D контента</span>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button id="saveBtn" class="btn btn-primary">Сохранить настройки</button>
|
||||
<button id="createFoldersBtn" class="btn btn-secondary">Создать папки</button>
|
||||
<button id="startCaptureBtn" class="btn btn-success">Запустить захват</button>
|
||||
</div>
|
||||
|
||||
<div class="cheatsheet-note">
|
||||
<p><strong>Шпаргалка (исходные данные)</strong></p>
|
||||
<p>Дроновые виды (query): Платформа Синтетический рендеринг (Google Earth Studio 3D). Тип съёмки RGB, top-down (камера вертикально вниз, pitch ~90°). Разрешение кадров 512x512 px (подтверждено из JSON и реальных файлов). FOV 30° по вертикали (из camera trajectory JSON). Частота кадров 30 fps, 2.5 сек на траекторию. Кадров на траекторию 76 (75 кадров анимации + начальный). Формат JPEG.</p>
|
||||
<p>Спутниковые виды (gallery / DB): Платформа Спутник (Google Earth / Google Maps tiles). Формат PNG (кропы), GeoTIFF (merge.tif). Разрешение кропов от 100x100 до 1000x1000 px (11 уникальных размеров, см. раздел 6). GSD варьируется по сценам (зависит от zoom level при рендеринге).</p>
|
||||
<p>Раздел 6: Параметр нарезки. Стратегия Sliding window с фиксированным overlap. Overlap 50% (stride = crop_size / 2). Именование crop_X_Y.png — X по ширине (col), Y по высоте (row). Позиция в merge.tif: merge[Y*stride : Y*stride+crop_size, X*stride : X*stride+crop_size].</p>
|
||||
<p>5.2. Параметры съёмки дронов: Высоты полёта 100 м, 125 м, 150 м (3 значения). Азимуты камеры (heading) 0°, 45°, 90°, 135°, 180°, 225°, 270°, 315° (8 направлений, шаг 45°). Число вариантов на сцену 24 (3 высоты x 8 азимутов) для Country; 8-24 для Terrain. FOV камеры 30° (вертикальный, подтверждено).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Информационная панель -->
|
||||
<div class="card info-card">
|
||||
<h2>Информация</h2>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Текущие координаты:</span>
|
||||
<span class="info-value" id="currentCoords">Не установлены</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Размер сетки:</span>
|
||||
<span class="info-value" id="currentGrid">2x2</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Всего точек:</span>
|
||||
<span class="info-value" id="totalPoints">4</span>
|
||||
</div>
|
||||
|
||||
<div class="info-item">
|
||||
<span class="info-label">Статус:</span>
|
||||
<span class="info-value status" id="status">Готов к работе</span>
|
||||
</div>
|
||||
|
||||
<div class="log-container">
|
||||
<h3>Лог операций</h3>
|
||||
<div id="logOutput" class="log-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Список созданных папок -->
|
||||
<div class="card folders-card">
|
||||
<h2>Созданные папки</h2>
|
||||
<div id="foldersList" class="folders-list">
|
||||
<p class="empty-state">Папки еще не созданы</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="ui.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
330
mapview.html
Normal file
330
mapview.html
Normal file
@@ -0,0 +1,330 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Карта зоны</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #0f172a;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
#bar {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 1rem;
|
||||
background: #1e293b;
|
||||
border-bottom: 1px solid #334155;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
#status {
|
||||
font-size: 0.9rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
#canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: calc(100% - 44px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="bar">
|
||||
<span id="title">Карта зоны</span>
|
||||
<span id="status">Ожидание данных...</span>
|
||||
<span id="debug" style="margin-left:auto;font-size:0.75rem;color:#64748b;">rx: 0</span>
|
||||
</div>
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<script>
|
||||
const statusEl = document.getElementById('status');
|
||||
const debugEl = document.getElementById('debug');
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const PADDING = 30;
|
||||
|
||||
let state = null;
|
||||
let rxCount = 0;
|
||||
let mapImage = null;
|
||||
let mapImageKey = '';
|
||||
let mapImageLoading = false;
|
||||
|
||||
function fitCanvas() {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
|
||||
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
}
|
||||
|
||||
function computeViewport() {
|
||||
if (!state) return null;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const w = Math.max(1, rect.width - PADDING * 2);
|
||||
const h = Math.max(1, rect.height - PADDING * 2);
|
||||
const { bounds } = state;
|
||||
const lonRange = Math.max(1e-9, bounds.maxLon - bounds.minLon);
|
||||
const latRange = Math.max(1e-9, bounds.maxLat - bounds.minLat);
|
||||
const midLat = (bounds.maxLat + bounds.minLat) / 2;
|
||||
const aspectGeo = (lonRange * Math.cos((midLat * Math.PI) / 180)) / latRange;
|
||||
const aspectCanvas = w / h;
|
||||
let sw;
|
||||
let sh;
|
||||
if (aspectGeo > aspectCanvas) {
|
||||
sw = w;
|
||||
sh = w / aspectGeo;
|
||||
} else {
|
||||
sh = h;
|
||||
sw = h * aspectGeo;
|
||||
}
|
||||
const ox = PADDING + (w - sw) / 2;
|
||||
const oy = PADDING + (h - sh) / 2;
|
||||
return { ox, oy, sw, sh, lonRange, latRange, bounds };
|
||||
}
|
||||
|
||||
function project(lat, lon, vp) {
|
||||
const x = vp.ox + ((lon - vp.bounds.minLon) / vp.lonRange) * vp.sw;
|
||||
const y = vp.oy + ((vp.bounds.maxLat - lat) / vp.latRange) * vp.sh;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function buildMapImageUrl(vp) {
|
||||
const w = Math.max(128, Math.floor(vp.sw));
|
||||
const h = Math.max(128, Math.floor(vp.sh));
|
||||
const bbox = [
|
||||
vp.bounds.minLon,
|
||||
vp.bounds.minLat,
|
||||
vp.bounds.maxLon,
|
||||
vp.bounds.maxLat
|
||||
].join(',');
|
||||
return `https://staticmap.openstreetmap.de/staticmap.php?bbox=${bbox}&size=${w}x${h}&maptype=mapnik`;
|
||||
}
|
||||
|
||||
function ensureMapImage(vp) {
|
||||
const key = [
|
||||
vp.bounds.minLon.toFixed(6),
|
||||
vp.bounds.minLat.toFixed(6),
|
||||
vp.bounds.maxLon.toFixed(6),
|
||||
vp.bounds.maxLat.toFixed(6),
|
||||
Math.floor(vp.sw),
|
||||
Math.floor(vp.sh)
|
||||
].join('|');
|
||||
|
||||
if (key === mapImageKey || mapImageLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
mapImageLoading = true;
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => {
|
||||
mapImage = img;
|
||||
mapImageKey = key;
|
||||
mapImageLoading = false;
|
||||
draw();
|
||||
};
|
||||
img.onerror = () => {
|
||||
mapImageLoading = false;
|
||||
};
|
||||
img.src = buildMapImageUrl(vp);
|
||||
}
|
||||
|
||||
function drawGrid(vp) {
|
||||
ctx.strokeStyle = '#1e293b';
|
||||
ctx.lineWidth = 1;
|
||||
const steps = 8;
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const x = vp.ox + (vp.sw * i) / steps;
|
||||
ctx.moveTo(x, vp.oy);
|
||||
ctx.lineTo(x, vp.oy + vp.sh);
|
||||
}
|
||||
for (let j = 0; j <= steps; j++) {
|
||||
const y = vp.oy + (vp.sh * j) / steps;
|
||||
ctx.moveTo(vp.ox, y);
|
||||
ctx.lineTo(vp.ox + vp.sw, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function draw() {
|
||||
fitCanvas();
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.fillStyle = '#0f172a';
|
||||
ctx.fillRect(0, 0, rect.width, rect.height);
|
||||
if (!state) return;
|
||||
|
||||
const vp = computeViewport();
|
||||
if (!vp) return;
|
||||
|
||||
ensureMapImage(vp);
|
||||
if (mapImage) {
|
||||
ctx.drawImage(mapImage, vp.ox, vp.oy, vp.sw, vp.sh);
|
||||
} else {
|
||||
ctx.fillStyle = 'rgba(239, 68, 68, 0.08)';
|
||||
ctx.fillRect(vp.ox, vp.oy, vp.sw, vp.sh);
|
||||
}
|
||||
drawGrid(vp);
|
||||
ctx.strokeStyle = '#ef4444';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(vp.ox, vp.oy, vp.sw, vp.sh);
|
||||
|
||||
ctx.fillStyle = '#38bdf8';
|
||||
state.tiles.forEach((tile) => {
|
||||
const p = project(tile.lat, tile.lon, vp);
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 2.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
ctx.fillStyle = '#22c55e';
|
||||
state.visited.forEach((v) => {
|
||||
const p = project(v.lat, v.lon, vp);
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
if (state.active) {
|
||||
const p = project(state.active.lat, state.active.lon, vp);
|
||||
ctx.fillStyle = state.done ? '#10b981' : '#fbbf24';
|
||||
ctx.strokeStyle = state.done ? '#064e3b' : '#92400e';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, 9, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function setInit(bounds, tiles) {
|
||||
state = {
|
||||
bounds,
|
||||
tiles: Array.isArray(tiles) ? tiles : [],
|
||||
visited: [],
|
||||
active: null,
|
||||
done: false,
|
||||
total: Array.isArray(tiles) ? tiles.length : 0
|
||||
};
|
||||
mapImage = null;
|
||||
mapImageKey = '';
|
||||
}
|
||||
|
||||
let lastInitTs = 0;
|
||||
let lastPosTs = 0;
|
||||
let lastDoneTs = 0;
|
||||
|
||||
function handleMessage(msg, source) {
|
||||
if (!msg || !msg.type) return;
|
||||
rxCount += 1;
|
||||
debugEl.textContent = `rx: ${rxCount} (${msg.type}/${source})`;
|
||||
if (msg.type === 'init') {
|
||||
if (msg.ts && msg.ts <= lastInitTs) return;
|
||||
lastInitTs = msg.ts || Date.now();
|
||||
setInit(msg.bounds, msg.tiles);
|
||||
statusEl.textContent = `Готов. Точек: ${state.total}`;
|
||||
draw();
|
||||
} else if (msg.type === 'position') {
|
||||
if (msg.ts && msg.ts <= lastPosTs) return;
|
||||
lastPosTs = msg.ts || Date.now();
|
||||
if (!state && msg.bounds) {
|
||||
setInit(msg.bounds, msg.tiles);
|
||||
state.total = msg.total || state.total;
|
||||
}
|
||||
if (!state) return;
|
||||
if (state.active) state.visited.push(state.active);
|
||||
state.active = { lat: msg.lat, lon: msg.lon };
|
||||
statusEl.textContent = `Съёмка: ${msg.index} / ${msg.total} (${msg.lat.toFixed(5)}, ${msg.lon.toFixed(5)})`;
|
||||
draw();
|
||||
} else if (msg.type === 'done') {
|
||||
if (msg.ts && msg.ts <= lastDoneTs) return;
|
||||
lastDoneTs = msg.ts || Date.now();
|
||||
if (!state) return;
|
||||
state.done = true;
|
||||
statusEl.textContent = `Готово. Кадров: ${msg.total ?? state.total}`;
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
function pollLocalStorage() {
|
||||
try {
|
||||
const initRaw = localStorage.getItem('sandcastle-map-init');
|
||||
if (initRaw) {
|
||||
const msg = JSON.parse(initRaw);
|
||||
if (msg.ts && msg.ts > lastInitTs) handleMessage(msg, 'ls');
|
||||
}
|
||||
const posRaw = localStorage.getItem('sandcastle-map-pos');
|
||||
if (posRaw) {
|
||||
const msg = JSON.parse(posRaw);
|
||||
if (msg.ts && msg.ts > lastPosTs) handleMessage(msg, 'ls');
|
||||
}
|
||||
const doneRaw = localStorage.getItem('sandcastle-map-done');
|
||||
if (doneRaw) {
|
||||
const msg = JSON.parse(doneRaw);
|
||||
if (msg.ts && msg.ts > lastDoneTs) handleMessage(msg, 'ls');
|
||||
}
|
||||
} catch (e) {}
|
||||
setTimeout(pollLocalStorage, 500);
|
||||
}
|
||||
|
||||
window.addEventListener('storage', (ev) => {
|
||||
if (!ev.key || !ev.newValue) return;
|
||||
if (!ev.key.startsWith('sandcastle-map-')) return;
|
||||
try {
|
||||
handleMessage(JSON.parse(ev.newValue), 'evt');
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
let channel = null;
|
||||
try {
|
||||
channel = new BroadcastChannel('sandcastle-map');
|
||||
} catch (e) {
|
||||
debugEl.textContent = 'BroadcastChannel не поддерживается';
|
||||
}
|
||||
if (channel) {
|
||||
channel.onmessage = (ev) => handleMessage(ev.data, 'bc');
|
||||
}
|
||||
|
||||
window.addEventListener('resize', draw);
|
||||
fitCanvas();
|
||||
draw();
|
||||
pollLocalStorage();
|
||||
|
||||
function requestInit() {
|
||||
if (state) return;
|
||||
if (channel) {
|
||||
try { channel.postMessage({ type: 'request-init' }); } catch (e) {}
|
||||
}
|
||||
setTimeout(requestInit, 1000);
|
||||
}
|
||||
requestInit();
|
||||
|
||||
setTimeout(() => {
|
||||
if (state) return;
|
||||
const hasBC = !!channel;
|
||||
const hasLS = (() => { try { localStorage.setItem('__t', '1'); localStorage.removeItem('__t'); return true; } catch (e) { return false; } })();
|
||||
statusEl.textContent = `Нет данных ${rxCount}с. BC=${hasBC} LS=${hasLS}. Захват запущен?`;
|
||||
}, 3000);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
854
package-lock.json
generated
Normal file
854
package-lock.json
generated
Normal file
@@ -0,0 +1,854 @@
|
||||
{
|
||||
"name": "sandcastle",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sandcastle",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"pngjs": "^7.0.0",
|
||||
"utif": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.14.0",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.3",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.14.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/utif": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/utif/-/utif-3.1.0.tgz",
|
||||
"integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "sandcastle",
|
||||
"version": "1.0.0",
|
||||
"description": "SandCastle - Координаты и захват изображений",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [
|
||||
"cesium",
|
||||
"coordinates",
|
||||
"capture"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"pngjs": "^7.0.0",
|
||||
"utif": "^3.1.0"
|
||||
}
|
||||
}
|
||||
503
server.js
Normal file
503
server.js
Normal file
@@ -0,0 +1,503 @@
|
||||
const express = require("express");
|
||||
const fs = require("fs").promises;
|
||||
const path = require("path");
|
||||
const { PNG } = require("pngjs");
|
||||
const UTIF = require("utif");
|
||||
|
||||
const app = express();
|
||||
const PORT = Math.max(1, Number.parseInt(process.env.PORT, 10) || 3000);
|
||||
|
||||
app.use(express.json({ limit: "50mb" }));
|
||||
app.use(express.static(__dirname));
|
||||
|
||||
const SETTINGS_FILE = path.join(__dirname, "settings.json");
|
||||
const OUTPUT_DIR = path.join(__dirname, "output");
|
||||
const METERS_PER_DEG_LAT = 111320;
|
||||
const DMS_VALUE_REGEX = /^\s*(\d+)\s*(?:°|В°)\s*(\d+)\s*'\s*([\d.]+)\s*"?\s*([NSEW])\s*$/i;
|
||||
const DMS_TOKEN_REGEX = /\d+\s*(?:°|В°)\s*\d+\s*'\s*[\d.]+\s*"?\s*[NSEW]/gi;
|
||||
|
||||
function metersToLat(meters) {
|
||||
return meters / METERS_PER_DEG_LAT;
|
||||
}
|
||||
|
||||
function metersToLon(meters, latDeg) {
|
||||
return meters / (METERS_PER_DEG_LAT * Math.cos((latDeg * Math.PI) / 180));
|
||||
}
|
||||
|
||||
function dmsToDecimal(dms) {
|
||||
const match = String(dms).trim().match(DMS_VALUE_REGEX);
|
||||
if (!match) {
|
||||
throw new Error(`Неверный формат координат: ${dms}`);
|
||||
}
|
||||
|
||||
const degrees = Number.parseInt(match[1], 10);
|
||||
const minutes = Number.parseInt(match[2], 10);
|
||||
const seconds = Number.parseFloat(match[3]);
|
||||
const direction = match[4].toUpperCase();
|
||||
|
||||
let decimal = degrees + minutes / 60 + seconds / 3600;
|
||||
if (direction === "S" || direction === "W") {
|
||||
decimal = -decimal;
|
||||
}
|
||||
|
||||
return decimal;
|
||||
}
|
||||
|
||||
function parseCoordinatePair(value) {
|
||||
const tokens = String(value || "").match(DMS_TOKEN_REGEX) || [];
|
||||
if (tokens.length !== 2) {
|
||||
throw new Error(`Ожидалась пара DMS (широта и долгота): ${value}`);
|
||||
}
|
||||
|
||||
const coords = tokens.map((token) => ({
|
||||
value: dmsToDecimal(token),
|
||||
direction: token.trim().slice(-1).toUpperCase()
|
||||
}));
|
||||
|
||||
const lat = coords.find((c) => c.direction === "N" || c.direction === "S");
|
||||
const lon = coords.find((c) => c.direction === "E" || c.direction === "W");
|
||||
|
||||
if (!lat || !lon) {
|
||||
throw new Error(`В паре должны быть широта и долгота: ${value}`);
|
||||
}
|
||||
|
||||
return { lat: lat.value, lon: lon.value };
|
||||
}
|
||||
|
||||
function getBoundsFromSettings(settings) {
|
||||
const text1 = String(settings.point1 || "").trim();
|
||||
const text2 = String(settings.point2 || "").trim();
|
||||
|
||||
if (!text1 || !text2) {
|
||||
throw new Error("Координаты не указаны");
|
||||
}
|
||||
|
||||
const p1 = parseCoordinatePair(text1);
|
||||
const p2 = parseCoordinatePair(text2);
|
||||
|
||||
return {
|
||||
minLat: Math.min(p1.lat, p2.lat),
|
||||
maxLat: Math.max(p1.lat, p2.lat),
|
||||
minLon: Math.min(p1.lon, p2.lon),
|
||||
maxLon: Math.max(p1.lon, p2.lon)
|
||||
};
|
||||
}
|
||||
|
||||
function decimalToDMS(decimal, isLatitude) {
|
||||
const absolute = Math.abs(decimal);
|
||||
const degrees = Math.floor(absolute);
|
||||
const minutesFloat = (absolute - degrees) * 60;
|
||||
let minutes = Math.floor(minutesFloat);
|
||||
let seconds = Math.round((minutesFloat - minutes) * 60);
|
||||
|
||||
if (seconds === 60) {
|
||||
seconds = 0;
|
||||
minutes += 1;
|
||||
}
|
||||
|
||||
const direction = isLatitude
|
||||
? decimal >= 0 ? "N" : "S"
|
||||
: decimal >= 0 ? "E" : "W";
|
||||
|
||||
return `${degrees}°${minutes}'${seconds}"${direction}`;
|
||||
}
|
||||
|
||||
function normalizeSettings(rawSettings) {
|
||||
const source = rawSettings || {};
|
||||
const settings = {
|
||||
dataset: String(source.dataset || "Country").trim() || "Country",
|
||||
country: String(source.country || "Custom").trim() || "Custom",
|
||||
city: String(source.city || "Scene").trim() || "Scene",
|
||||
region: String(source.region || "Region").trim() || "Region",
|
||||
point1: String(source.point1 || "").trim(),
|
||||
point2: String(source.point2 || "").trim(),
|
||||
tile_m: Number(source.tile_m),
|
||||
grid_step: Math.max(1, Number.parseInt(source.grid_step, 10) || 1),
|
||||
height_100: source.height_100 !== undefined ? Boolean(source.height_100) : true,
|
||||
height_125: source.height_125 !== undefined ? Boolean(source.height_125) : true,
|
||||
height_150: source.height_150 !== undefined ? Boolean(source.height_150) : true,
|
||||
height_100_value: Math.max(1, Number(source.height_100_value) || 100),
|
||||
height_125_value: Math.max(1, Number(source.height_125_value) || 125),
|
||||
height_150_value: Math.max(1, Number(source.height_150_value) || 150),
|
||||
height_absolute: source.height_absolute !== undefined ? Boolean(source.height_absolute) : true,
|
||||
db_crop_sizes: String(source.db_crop_sizes || "100,150,200,250,300,400,500,512,600,800,1000").trim(),
|
||||
drone_size: Math.max(1, Number.parseInt(source.drone_size, 10) || 512),
|
||||
query_fov_deg: Math.min(179, Math.max(1, Number(source.query_fov_deg) || 30)),
|
||||
query_pitch_deg: -90,
|
||||
preload: source.preload !== undefined ? Boolean(source.preload) : true,
|
||||
skip_empty: Boolean(source.skip_empty)
|
||||
};
|
||||
|
||||
const uniqueDbSizes = Array.from(new Set(
|
||||
String(settings.db_crop_sizes || "")
|
||||
.split(",")
|
||||
.map((v) => Number.parseInt(v.trim(), 10))
|
||||
.filter((v) => Number.isInteger(v) && v > 0)
|
||||
));
|
||||
|
||||
if (!Number.isFinite(settings.tile_m) || settings.tile_m <= 0) {
|
||||
throw new Error("Tile (метры) должен быть больше 0");
|
||||
}
|
||||
if (!settings.height_100 && !settings.height_125 && !settings.height_150) {
|
||||
throw new Error("Выберите хотя бы одну высоту (100/125/150)");
|
||||
}
|
||||
if (uniqueDbSizes.length !== 11) {
|
||||
throw new Error("DB crop sizes: нужно ровно 11 уникальных размеров");
|
||||
}
|
||||
|
||||
settings.db_crop_sizes = uniqueDbSizes.join(",");
|
||||
getBoundsFromSettings(settings);
|
||||
return settings;
|
||||
}
|
||||
|
||||
function normalizeSceneSettings(rawSettings) {
|
||||
const source = rawSettings || {};
|
||||
return {
|
||||
dataset: String(source.dataset || "Country").trim() || "Country",
|
||||
country: String(source.country || "Custom").trim() || "Custom",
|
||||
city: String(source.city || "Scene").trim() || "Scene",
|
||||
region: String(source.region || "Region").trim() || "Region"
|
||||
};
|
||||
}
|
||||
|
||||
function computeGrid(bounds, tileMeters) {
|
||||
const anchorLat = bounds.maxLat;
|
||||
const latSpanM = (bounds.maxLat - bounds.minLat) * METERS_PER_DEG_LAT;
|
||||
const lonSpanM = (bounds.maxLon - bounds.minLon) * METERS_PER_DEG_LAT * Math.cos((anchorLat * Math.PI) / 180);
|
||||
const cols = Math.max(1, Math.floor(lonSpanM / tileMeters) + 1);
|
||||
const rows = Math.max(1, Math.floor(latSpanM / tileMeters) + 1);
|
||||
return { cols, rows };
|
||||
}
|
||||
|
||||
function generateCoordinates(settings) {
|
||||
const bounds = getBoundsFromSettings(settings);
|
||||
const tileMeters = Math.max(1, Number(settings.tile_m) || 1);
|
||||
const step = Math.max(1, settings.grid_step || 1);
|
||||
const { cols, rows } = computeGrid(bounds, tileMeters);
|
||||
|
||||
const coordinates = [];
|
||||
let index = 1;
|
||||
|
||||
for (let row = 0; row < rows; row += step) {
|
||||
for (let col = 0; col < cols; col += step) {
|
||||
const lat = bounds.maxLat - metersToLat(row * tileMeters);
|
||||
const lon = bounds.minLon + metersToLon(col * tileMeters, lat);
|
||||
|
||||
coordinates.push({
|
||||
index: index++,
|
||||
row,
|
||||
col,
|
||||
lat,
|
||||
lon,
|
||||
latDMS: decimalToDMS(lat, true),
|
||||
lonDMS: decimalToDMS(lon, false)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { coordinates, bounds, gridRows: rows, gridCols: cols };
|
||||
}
|
||||
|
||||
function dataUrlToBuffer(dataUrl) {
|
||||
if (typeof dataUrl !== "string") {
|
||||
throw new Error("Некорректные данные изображения");
|
||||
}
|
||||
|
||||
const match = dataUrl.match(/^data:image\/(png|jpeg);base64,(.+)$/);
|
||||
if (!match) {
|
||||
throw new Error("Ожидался PNG или JPEG в формате data URL");
|
||||
}
|
||||
|
||||
return Buffer.from(match[2], "base64");
|
||||
}
|
||||
|
||||
function pngBufferToTiffBuffer(pngBuffer) {
|
||||
const png = PNG.sync.read(pngBuffer);
|
||||
return Buffer.from(UTIF.encodeImage(png.data, png.width, png.height));
|
||||
}
|
||||
|
||||
function sanitizeNameSegment(name, fallback) {
|
||||
const clean = String(name || "")
|
||||
.trim()
|
||||
.replace(/[\\/:*?"<>|]/g, "_")
|
||||
.replace(/\s+/g, "_");
|
||||
return clean || fallback;
|
||||
}
|
||||
|
||||
function getScenePath(settings) {
|
||||
return path.join(
|
||||
OUTPUT_DIR,
|
||||
sanitizeNameSegment(settings.dataset, "Country"),
|
||||
sanitizeNameSegment(settings.country, "Custom"),
|
||||
sanitizeNameSegment(settings.city, "Scene"),
|
||||
sanitizeNameSegment(settings.region, "Region")
|
||||
);
|
||||
}
|
||||
|
||||
app.post("/api/save-settings", async (req, res) => {
|
||||
try {
|
||||
const settings = normalizeSettings(req.body);
|
||||
await fs.writeFile(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
||||
res.json({ success: true, message: "Настройки сохранены" });
|
||||
} catch (error) {
|
||||
console.error("Ошибка сохранения настроек:", error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/create-folders", async (req, res) => {
|
||||
try {
|
||||
const settings = normalizeSettings(req.body);
|
||||
const { coordinates, bounds, gridRows, gridCols } = generateCoordinates(settings);
|
||||
const scenePath = getScenePath(settings);
|
||||
const regionName = sanitizeNameSegment(settings.region, "Region");
|
||||
|
||||
await fs.mkdir(path.join(scenePath, "DB", "img"), { recursive: true });
|
||||
await fs.mkdir(path.join(scenePath, "query"), { recursive: true });
|
||||
|
||||
await fs.writeFile(path.join(scenePath, "DB", "db_postion.txt"), "");
|
||||
|
||||
const regionTxt =
|
||||
`${bounds.maxLat} ${bounds.minLon}\n` +
|
||||
`${bounds.minLat} ${bounds.maxLon}\n`;
|
||||
await fs.writeFile(path.join(scenePath, `${regionName}.txt`), regionTxt);
|
||||
|
||||
const folders = [
|
||||
{
|
||||
name: regionName,
|
||||
path: scenePath,
|
||||
coordinates_count: coordinates.length
|
||||
}
|
||||
];
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
folders,
|
||||
gridRows,
|
||||
gridCols,
|
||||
bounds,
|
||||
pointsCount: coordinates.length,
|
||||
scenePath,
|
||||
message: `Сцена создана, точек: ${coordinates.length}`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Ошибка создания сцены:", error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/save-db-crop", async (req, res) => {
|
||||
try {
|
||||
const settings = normalizeSceneSettings(req.body);
|
||||
const col = Number.parseInt(req.body.col, 10);
|
||||
const row = Number.parseInt(req.body.row, 10);
|
||||
const lat = Number(req.body.lat);
|
||||
const lon = Number(req.body.lon);
|
||||
const resX = Number.parseInt(req.body.res_x, 10) || 0;
|
||||
const resY = Number.parseInt(req.body.res_y, 10) || 0;
|
||||
|
||||
if (!Number.isInteger(col) || !Number.isInteger(row)) {
|
||||
throw new Error("Некорректные col/row");
|
||||
}
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
|
||||
throw new Error("Некорректные координаты crop");
|
||||
}
|
||||
|
||||
const pngBuffer = dataUrlToBuffer(req.body.png);
|
||||
const scenePath = getScenePath(settings);
|
||||
const imgDir = path.join(scenePath, "DB", "img");
|
||||
await fs.mkdir(imgDir, { recursive: true });
|
||||
|
||||
const sizeTag = Number.parseInt(req.body.size_tag, 10);
|
||||
const suffix = Number.isInteger(sizeTag) && sizeTag > 0 ? `_${sizeTag}` : "";
|
||||
const filename = `crop_${col}_${row}${suffix}.png`;
|
||||
await fs.writeFile(path.join(imgDir, filename), pngBuffer);
|
||||
|
||||
const line = `${filename} ${lon} ${lat} ${resX} ${resY}\n`;
|
||||
await fs.appendFile(path.join(scenePath, "DB", "db_postion.txt"), line);
|
||||
|
||||
res.json({ success: true, filename });
|
||||
} catch (error) {
|
||||
console.error("Ошибка сохранения DB crop:", error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/save-merge-tif", async (req, res) => {
|
||||
try {
|
||||
const settings = normalizeSceneSettings(req.body);
|
||||
const pngBuffer = dataUrlToBuffer(req.body.png);
|
||||
const tiffBuffer = pngBufferToTiffBuffer(pngBuffer);
|
||||
const bounds = getBoundsFromSettings(req.body);
|
||||
const scenePath = getScenePath(settings);
|
||||
const dbDir = path.join(scenePath, "DB");
|
||||
await fs.mkdir(dbDir, { recursive: true });
|
||||
await fs.writeFile(path.join(dbDir, "merge.tif"), tiffBuffer);
|
||||
await fs.writeFile(path.join(dbDir, "geo.tif"), tiffBuffer);
|
||||
await fs.writeFile(
|
||||
path.join(dbDir, "geo_bounds.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
crs: "EPSG:4326",
|
||||
bounds
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
res.json({ success: true, files: ["merge.tif", "geo.tif", "geo_bounds.json"] });
|
||||
} catch (error) {
|
||||
console.error("Ошибка сохранения merge.tif:", error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/save-drone-frame", async (req, res) => {
|
||||
try {
|
||||
const settings = normalizeSceneSettings(req.body);
|
||||
const heightValue = Number(req.body.height);
|
||||
const rotValue = Number.isFinite(Number(req.body.rot)) ? Number(req.body.rot) : 0;
|
||||
const frameIdx = Number.parseInt(req.body.frameIdx, 10);
|
||||
|
||||
if (!Number.isFinite(heightValue) || heightValue <= 0) {
|
||||
throw new Error("Некорректная высота");
|
||||
}
|
||||
if (!Number.isInteger(frameIdx) || frameIdx < 0) {
|
||||
throw new Error("Некорректный frameIdx");
|
||||
}
|
||||
|
||||
const jpegBuffer = dataUrlToBuffer(req.body.jpeg);
|
||||
const scenePath = getScenePath(settings);
|
||||
const variantName = `height${heightValue}_rot${rotValue}`;
|
||||
const footageDir = path.join(scenePath, "query", variantName, "footage");
|
||||
await fs.mkdir(footageDir, { recursive: true });
|
||||
|
||||
const frameStr = String(frameIdx).padStart(2, "0");
|
||||
const filename = `${variantName}_${frameStr}.jpeg`;
|
||||
await fs.writeFile(path.join(footageDir, filename), jpegBuffer);
|
||||
|
||||
res.json({ success: true, filename });
|
||||
} catch (error) {
|
||||
console.error("Ошибка сохранения drone frame:", error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/save-trajectory", async (req, res) => {
|
||||
try {
|
||||
const settings = normalizeSceneSettings(req.body);
|
||||
const heightValue = Number(req.body.height);
|
||||
const rotValue = Number.isFinite(Number(req.body.rot)) ? Number(req.body.rot) : 0;
|
||||
const trajectory = req.body.trajectory;
|
||||
|
||||
if (!Number.isFinite(heightValue) || heightValue <= 0) {
|
||||
throw new Error("Некорректная высота");
|
||||
}
|
||||
if (!trajectory || !Array.isArray(trajectory.cameraFrames)) {
|
||||
throw new Error("Некорректная траектория");
|
||||
}
|
||||
|
||||
const scenePath = getScenePath(settings);
|
||||
const variantName = `height${heightValue}_rot${rotValue}`;
|
||||
const variantDir = path.join(scenePath, "query", variantName);
|
||||
await fs.mkdir(variantDir, { recursive: true });
|
||||
|
||||
const filePath = path.join(variantDir, `${variantName}.json`);
|
||||
await fs.writeFile(filePath, JSON.stringify(trajectory, null, 2));
|
||||
|
||||
res.json({ success: true, filename: `${variantName}.json` });
|
||||
} catch (error) {
|
||||
console.error("Ошибка сохранения trajectory:", error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/save-annotations", async (req, res) => {
|
||||
try {
|
||||
const settings = normalizeSceneSettings(req.body);
|
||||
const positive = req.body.positive || {};
|
||||
const semiPositive = req.body.semi_positive || {};
|
||||
|
||||
if (typeof positive !== "object" || Array.isArray(positive) ||
|
||||
typeof semiPositive !== "object" || Array.isArray(semiPositive)) {
|
||||
throw new Error("Некорректные аннотации");
|
||||
}
|
||||
|
||||
const scenePath = getScenePath(settings);
|
||||
await fs.mkdir(scenePath, { recursive: true });
|
||||
await fs.writeFile(path.join(scenePath, "positive.json"), JSON.stringify(positive, null, 2));
|
||||
await fs.writeFile(path.join(scenePath, "semi_positive.json"), JSON.stringify(semiPositive, null, 2));
|
||||
|
||||
res.json({ success: true, files: ["positive.json", "semi_positive.json"] });
|
||||
} catch (error) {
|
||||
console.error("Ошибка сохранения аннотаций:", error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/start-capture", async (req, res) => {
|
||||
try {
|
||||
const settings = normalizeSettings(req.body);
|
||||
await fs.writeFile(SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
url: `http://localhost:${PORT}/capture.html`,
|
||||
message: "Захват запущен"
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Ошибка запуска захвата:", error);
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/get-settings", async (req, res) => {
|
||||
try {
|
||||
const data = await fs.readFile(SETTINGS_FILE, "utf8");
|
||||
const settings = JSON.parse(data);
|
||||
settings.query_pitch_deg = -90;
|
||||
res.json({ success: true, settings });
|
||||
} catch (error) {
|
||||
res.json({
|
||||
success: true,
|
||||
settings: {
|
||||
dataset: "Country",
|
||||
country: "German",
|
||||
city: "Hannover",
|
||||
region: "Custom",
|
||||
point1: `52°25'10.28"N 9°39'10.04"E`,
|
||||
point2: `52°25'07.34"N 9°39'23.65"E`,
|
||||
tile_m: 49,
|
||||
grid_step: 1,
|
||||
height_100: true,
|
||||
height_125: true,
|
||||
height_150: true,
|
||||
height_100_value: 100,
|
||||
height_125_value: 125,
|
||||
height_150_value: 150,
|
||||
height_absolute: true,
|
||||
db_crop_sizes: "100,150,200,250,300,400,500,512,600,800,1000",
|
||||
drone_size: 512,
|
||||
query_fov_deg: 30,
|
||||
query_pitch_deg: -90,
|
||||
preload: true,
|
||||
skip_empty: false
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`
|
||||
╔════════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
║ 🗺️ SandCastle Server запущен! ║
|
||||
║ ║
|
||||
║ 📌 URL: http://localhost:${PORT} ║
|
||||
║ 📂 Папка вывода: ${OUTPUT_DIR} ║
|
||||
║ ║
|
||||
║ Откройте браузер и перейдите по адресу выше ║
|
||||
║ ║
|
||||
╚════════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
});
|
||||
23
settings.json
Normal file
23
settings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"dataset": "Country",
|
||||
"country": "German",
|
||||
"city": "Milan",
|
||||
"region": "Milan",
|
||||
"point1": "45°28'24.38\"N 9°10'10.45\"E",
|
||||
"point2": "45°27'16.41\"N 9°12'24.26\"E",
|
||||
"tile_m": 49,
|
||||
"grid_step": 1,
|
||||
"height_100": false,
|
||||
"height_125": true,
|
||||
"height_150": true,
|
||||
"height_100_value": 100,
|
||||
"height_125_value": 100,
|
||||
"height_150_value": 125,
|
||||
"height_absolute": false,
|
||||
"db_crop_sizes": "100,150,200,250,300,400,500,512,600,800,1000",
|
||||
"drone_size": 512,
|
||||
"query_fov_deg": 30,
|
||||
"query_pitch_deg": -90,
|
||||
"preload": false,
|
||||
"skip_empty": false
|
||||
}
|
||||
57
start.bat
Normal file
57
start.bat
Normal file
@@ -0,0 +1,57 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
cls
|
||||
|
||||
echo.
|
||||
echo ╔════════════════════════════════════════════════════════════╗
|
||||
echo ║ ║
|
||||
echo ║ 🗺️ SandCastle - Запуск программы ║
|
||||
echo ║ ║
|
||||
echo ╚════════════════════════════════════════════════════════════╝
|
||||
echo.
|
||||
|
||||
REM Проверка наличия Node.js
|
||||
where node >nul 2>nul
|
||||
if %errorlevel% neq 0 (
|
||||
echo ❌ Node.js не установлен!
|
||||
echo.
|
||||
echo Пожалуйста, установите Node.js с https://nodejs.org/
|
||||
echo.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo ✅ Node.js найден
|
||||
echo.
|
||||
|
||||
REM Проверка наличия node_modules
|
||||
if not exist "node_modules" (
|
||||
echo 📦 Установка зависимостей...
|
||||
echo.
|
||||
call npm install
|
||||
echo.
|
||||
if %errorlevel% neq 0 (
|
||||
echo ❌ Ошибка установки зависимостей
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
echo ✅ Зависимости установлены
|
||||
echo.
|
||||
)
|
||||
|
||||
REM Запуск сервера
|
||||
echo 🚀 Запуск сервера...
|
||||
echo.
|
||||
|
||||
set "PORT=%~1"
|
||||
if "%PORT%"=="" set "PORT=3000"
|
||||
|
||||
echo 🌐 Порт: %PORT%
|
||||
echo.
|
||||
|
||||
start "" "http://localhost:%PORT%"
|
||||
|
||||
set "PORT=%PORT%"
|
||||
node server.js
|
||||
|
||||
pause
|
||||
530
style.css
Normal file
530
style.css
Normal file
@@ -0,0 +1,530 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--primary: #6366f1;
|
||||
--primary-dark: #4f46e5;
|
||||
--secondary: #8b5cf6;
|
||||
--success: #10b981;
|
||||
--success-dark: #059669;
|
||||
--danger: #ef4444;
|
||||
--warning: #f59e0b;
|
||||
--bg-dark: #0f172a;
|
||||
--bg-card: #1e293b;
|
||||
--bg-card-hover: #334155;
|
||||
--text-primary: #f1f5f9;
|
||||
--text-secondary: #94a3b8;
|
||||
--border: #334155;
|
||||
--shadow: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
animation: fadeInDown 0.6s ease-out;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Main Content Grid */
|
||||
.main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.main-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow: 0 10px 30px var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
transition: all 0.3s ease;
|
||||
animation: fadeInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 15px 40px var(--shadow);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
/* Form Elements */
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
background: var(--bg-dark);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
input[type="number"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.section-block {
|
||||
margin: 1.25rem 0;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(15, 23, 42, 0.35);
|
||||
}
|
||||
|
||||
.section-block h3 {
|
||||
margin-bottom: 0.9rem;
|
||||
font-size: 1rem;
|
||||
color: #c7d2fe;
|
||||
}
|
||||
|
||||
.height-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.height-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.height-item input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.height-item input[type="number"] {
|
||||
min-width: 70px;
|
||||
padding: 0.45rem 0.55rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.cheatsheet-note {
|
||||
margin-top: 1.5rem;
|
||||
font-size: 0.74rem;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.cheatsheet-note p {
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
padding: 1rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Inter', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-small {
|
||||
margin-top: 0.7rem;
|
||||
width: auto;
|
||||
min-width: unset;
|
||||
padding: 0.55rem 0.8rem;
|
||||
font-size: 0.82rem;
|
||||
border-radius: 0.55rem;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 1.2rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.btn span:not(.btn-icon) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, var(--secondary) 0%, #7c3aed 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(139, 92, 246, 0.4);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, var(--success) 0%, var(--success-dark) 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Info Panel */
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: var(--bg-dark);
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.info-item:hover {
|
||||
background: var(--bg-card-hover);
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background: var(--success);
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.status.working {
|
||||
background: var(--warning);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
/* Log Container */
|
||||
.log-container {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
.log-container h3 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.log-output {
|
||||
background: var(--bg-dark);
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.log-output::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.log-output::-webkit-scrollbar-track {
|
||||
background: var(--bg-card);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-output::-webkit-scrollbar-thumb {
|
||||
background: var(--primary);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-left: 3px solid var(--primary);
|
||||
padding-left: 0.75rem;
|
||||
animation: slideInLeft 0.3s ease-out;
|
||||
}
|
||||
|
||||
.log-entry.success {
|
||||
border-left-color: var(--success);
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
border-left-color: var(--danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.log-entry.info {
|
||||
border-left-color: var(--primary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Folders List */
|
||||
.folders-card {
|
||||
animation: fadeInUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
.folders-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.folder-item {
|
||||
background: var(--bg-dark);
|
||||
padding: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 2px solid var(--border);
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
animation: scaleIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.folder-item:hover {
|
||||
border-color: var(--primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(99, 102, 241, 0.2);
|
||||
}
|
||||
|
||||
.folder-item .folder-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.folder-item .folder-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
color: var(--text-secondary);
|
||||
padding: 2rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.folders-list {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.height-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
373
ui.js
Normal file
373
ui.js
Normal file
@@ -0,0 +1,373 @@
|
||||
// UI Controller for SandCastle
|
||||
|
||||
class SandCastleUI {
|
||||
constructor() {
|
||||
this.settings = {
|
||||
dataset: 'Country',
|
||||
country: 'German',
|
||||
city: 'Hannover',
|
||||
region: 'Custom',
|
||||
point1: '52°25\'10.28"N 9°39\'10.04"E',
|
||||
point2: '52°25\'07.34"N 9°39\'23.65"E',
|
||||
tile_m: 49,
|
||||
grid_step: 1,
|
||||
height_100: true,
|
||||
height_125: true,
|
||||
height_150: true,
|
||||
height_100_value: 100,
|
||||
height_125_value: 125,
|
||||
height_150_value: 150,
|
||||
height_absolute: true,
|
||||
db_crop_sizes: "100,150,200,250,300,400,500,512,600,800,1000",
|
||||
drone_size: 512,
|
||||
query_fov_deg: 30,
|
||||
query_pitch_deg: -90,
|
||||
preload: true,
|
||||
skip_empty: false
|
||||
};
|
||||
|
||||
this.METERS_PER_DEG_LAT = 111320;
|
||||
this.DEFAULT_DB_CROP_SIZES = '100,150,200,250,300,400,500,512,600,800,1000';
|
||||
this.DMS_VALUE_REGEX = /^\s*(\d+)\s*(?:°|В°)\s*(\d+)\s*'\s*([\d.]+)\s*"?\s*([NSEW])\s*$/i;
|
||||
this.DMS_TOKEN_REGEX = /\d+\s*(?:°|В°)\s*\d+\s*'\s*[\d.]+\s*"?\s*[NSEW]/gi;
|
||||
|
||||
this.folders = [];
|
||||
this.initializeElements();
|
||||
this.attachEventListeners();
|
||||
this.loadSettingsFromServer();
|
||||
this.updateInfo();
|
||||
}
|
||||
|
||||
initializeElements() {
|
||||
this.datasetInput = document.getElementById('dataset');
|
||||
this.countryInput = document.getElementById('country');
|
||||
this.cityInput = document.getElementById('city');
|
||||
this.regionInput = document.getElementById('region');
|
||||
this.point1Input = document.getElementById('point1');
|
||||
this.point2Input = document.getElementById('point2');
|
||||
this.tileMInput = document.getElementById('tile_m');
|
||||
this.gridStepInput = document.getElementById('grid_step');
|
||||
this.dbCropSizesInput = document.getElementById('db_crop_sizes');
|
||||
this.droneSizeInput = document.getElementById('drone_size');
|
||||
this.queryFovInput = document.getElementById('query_fov_deg');
|
||||
this.queryPitchInput = document.getElementById('query_pitch_deg');
|
||||
this.height100Input = document.getElementById('height_100');
|
||||
this.height125Input = document.getElementById('height_125');
|
||||
this.height150Input = document.getElementById('height_150');
|
||||
this.height100ValueInput = document.getElementById('height_100_value');
|
||||
this.height125ValueInput = document.getElementById('height_125_value');
|
||||
this.height150ValueInput = document.getElementById('height_150_value');
|
||||
this.heightAbsoluteInput = document.getElementById('height_absolute');
|
||||
this.preloadInput = document.getElementById('preload');
|
||||
this.skipEmptyInput = document.getElementById('skip_empty');
|
||||
this.fillDbSizesBtn = document.getElementById('fillDbSizesBtn');
|
||||
|
||||
this.saveBtn = document.getElementById('saveBtn');
|
||||
this.createFoldersBtn = document.getElementById('createFoldersBtn');
|
||||
this.startCaptureBtn = document.getElementById('startCaptureBtn');
|
||||
|
||||
this.currentCoords = document.getElementById('currentCoords');
|
||||
this.currentGrid = document.getElementById('currentGrid');
|
||||
this.totalPoints = document.getElementById('totalPoints');
|
||||
this.status = document.getElementById('status');
|
||||
this.logOutput = document.getElementById('logOutput');
|
||||
this.foldersList = document.getElementById('foldersList');
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
this.saveBtn.addEventListener('click', () => this.saveSettings());
|
||||
this.createFoldersBtn.addEventListener('click', () => this.createFolders());
|
||||
this.startCaptureBtn.addEventListener('click', () => this.startCapture());
|
||||
this.fillDbSizesBtn.addEventListener('click', () => {
|
||||
this.dbCropSizesInput.value = this.DEFAULT_DB_CROP_SIZES;
|
||||
this.updateInfo();
|
||||
this.log('Подставлены стандартные 11 размеров DB', 'info');
|
||||
});
|
||||
|
||||
[
|
||||
this.point1Input,
|
||||
this.point2Input,
|
||||
this.tileMInput,
|
||||
this.gridStepInput,
|
||||
this.height100Input,
|
||||
this.height125Input,
|
||||
this.height150Input,
|
||||
this.height100ValueInput,
|
||||
this.height125ValueInput,
|
||||
this.height150ValueInput,
|
||||
this.dbCropSizesInput,
|
||||
this.queryFovInput,
|
||||
this.queryPitchInput
|
||||
].forEach((input) => {
|
||||
const event = input.type === 'checkbox' ? 'change' : 'input';
|
||||
input.addEventListener(event, () => this.updateInfo());
|
||||
});
|
||||
}
|
||||
|
||||
log(message, type = 'info') {
|
||||
const entry = document.createElement('div');
|
||||
entry.className = `log-entry ${type}`;
|
||||
const timestamp = new Date().toLocaleTimeString('ru-RU');
|
||||
entry.textContent = `[${timestamp}] ${message}`;
|
||||
this.logOutput.appendChild(entry);
|
||||
this.logOutput.scrollTop = this.logOutput.scrollHeight;
|
||||
}
|
||||
|
||||
updateStatus(text, className = '') {
|
||||
this.status.textContent = text;
|
||||
this.status.className = 'info-value status ' + className;
|
||||
}
|
||||
|
||||
dmsToDecimal(dms) {
|
||||
const match = String(dms).trim().match(this.DMS_VALUE_REGEX);
|
||||
if (!match) {
|
||||
throw new Error(`Неверный формат DMS: ${dms}`);
|
||||
}
|
||||
|
||||
const degrees = Number.parseInt(match[1], 10);
|
||||
const minutes = Number.parseInt(match[2], 10);
|
||||
const seconds = Number.parseFloat(match[3]);
|
||||
const direction = match[4].toUpperCase();
|
||||
|
||||
let decimal = degrees + minutes / 60 + seconds / 3600;
|
||||
if (direction === 'S' || direction === 'W') {
|
||||
decimal = -decimal;
|
||||
}
|
||||
|
||||
return decimal;
|
||||
}
|
||||
|
||||
parseCoordinatePair(value) {
|
||||
const tokens = String(value || '').match(this.DMS_TOKEN_REGEX) || [];
|
||||
if (tokens.length !== 2) {
|
||||
throw new Error('Ожидалась пара DMS');
|
||||
}
|
||||
const coords = tokens.map((t) => ({
|
||||
value: this.dmsToDecimal(t),
|
||||
direction: t.trim().slice(-1).toUpperCase()
|
||||
}));
|
||||
const lat = coords.find((c) => c.direction === 'N' || c.direction === 'S');
|
||||
const lon = coords.find((c) => c.direction === 'E' || c.direction === 'W');
|
||||
if (!lat || !lon) {
|
||||
throw new Error('Нужны широта и долгота');
|
||||
}
|
||||
return { lat: lat.value, lon: lon.value };
|
||||
}
|
||||
|
||||
updateInfo() {
|
||||
const step = Math.max(1, Number.parseInt(this.gridStepInput.value, 10) || 1);
|
||||
const tileMeters = Math.max(1, Number(this.tileMInput.value) || 1);
|
||||
const heightCount =
|
||||
(this.height100Input.checked ? 1 : 0) +
|
||||
(this.height125Input.checked ? 1 : 0) +
|
||||
(this.height150Input.checked ? 1 : 0);
|
||||
|
||||
try {
|
||||
const p1 = this.parseCoordinatePair(this.point1Input.value);
|
||||
const p2 = this.parseCoordinatePair(this.point2Input.value);
|
||||
const minLat = Math.min(p1.lat, p2.lat);
|
||||
const maxLat = Math.max(p1.lat, p2.lat);
|
||||
const minLon = Math.min(p1.lon, p2.lon);
|
||||
const maxLon = Math.max(p1.lon, p2.lon);
|
||||
const latSpanM = (maxLat - minLat) * this.METERS_PER_DEG_LAT;
|
||||
const lonSpanM = (maxLon - minLon) * this.METERS_PER_DEG_LAT * Math.cos((maxLat * Math.PI) / 180);
|
||||
const cols = Math.max(1, Math.floor(lonSpanM / tileMeters) + 1);
|
||||
const rows = Math.max(1, Math.floor(latSpanM / tileMeters) + 1);
|
||||
const effectiveCols = Math.ceil(cols / step);
|
||||
const effectiveRows = Math.ceil(rows / step);
|
||||
const tiles = effectiveCols * effectiveRows;
|
||||
|
||||
this.currentGrid.textContent = `${effectiveCols}x${effectiveRows}`;
|
||||
this.totalPoints.textContent = heightCount > 0 ? `${tiles} × ${heightCount}в = ${tiles * heightCount}` : `${tiles} (нет высот)`;
|
||||
this.currentCoords.textContent = `${Math.round(lonSpanM)}×${Math.round(latSpanM)}м`;
|
||||
} catch (error) {
|
||||
this.currentGrid.textContent = '—';
|
||||
this.totalPoints.textContent = '—';
|
||||
}
|
||||
}
|
||||
|
||||
collectSettings() {
|
||||
const parsedDbSizes = this.parseDbCropSizes(this.dbCropSizesInput.value);
|
||||
if (parsedDbSizes.length !== 11) {
|
||||
throw new Error('DB crop sizes: укажите ровно 11 уникальных размеров');
|
||||
}
|
||||
|
||||
this.settings = {
|
||||
dataset: this.datasetInput.value.trim() || 'Country',
|
||||
country: this.countryInput.value.trim() || 'Custom',
|
||||
city: this.cityInput.value.trim() || 'Scene',
|
||||
region: this.regionInput.value.trim() || 'Region',
|
||||
point1: this.point1Input.value.trim(),
|
||||
point2: this.point2Input.value.trim(),
|
||||
tile_m: Math.max(1, Number(this.tileMInput.value) || 1),
|
||||
grid_step: Math.max(1, Number.parseInt(this.gridStepInput.value, 10) || 1),
|
||||
height_100: this.height100Input.checked,
|
||||
height_125: this.height125Input.checked,
|
||||
height_150: this.height150Input.checked,
|
||||
height_100_value: Math.max(1, Number(this.height100ValueInput.value) || 100),
|
||||
height_125_value: Math.max(1, Number(this.height125ValueInput.value) || 125),
|
||||
height_150_value: Math.max(1, Number(this.height150ValueInput.value) || 150),
|
||||
height_absolute: this.heightAbsoluteInput.checked,
|
||||
db_crop_sizes: parsedDbSizes.join(','),
|
||||
drone_size: Math.max(1, Number.parseInt(this.droneSizeInput.value, 10) || 512),
|
||||
query_fov_deg: Math.max(1, Math.min(179, Number(this.queryFovInput.value) || 30)),
|
||||
query_pitch_deg: -90,
|
||||
preload: this.preloadInput.checked,
|
||||
skip_empty: this.skipEmptyInput.checked
|
||||
};
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
parseDbCropSizes(raw) {
|
||||
return Array.from(new Set(
|
||||
String(raw || '')
|
||||
.split(',')
|
||||
.map((v) => Number.parseInt(v.trim(), 10))
|
||||
.filter((v) => Number.isInteger(v) && v > 0)
|
||||
));
|
||||
}
|
||||
|
||||
async loadSettingsFromServer() {
|
||||
try {
|
||||
const response = await fetch('/api/get-settings');
|
||||
const result = await response.json();
|
||||
if (!result.success || !result.settings) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = result.settings;
|
||||
this.datasetInput.value = settings.dataset ?? this.datasetInput.value;
|
||||
this.countryInput.value = settings.country ?? this.countryInput.value;
|
||||
this.cityInput.value = settings.city ?? this.cityInput.value;
|
||||
this.regionInput.value = settings.region ?? this.regionInput.value;
|
||||
this.point1Input.value = settings.point1 ?? this.point1Input.value;
|
||||
this.point2Input.value = settings.point2 ?? this.point2Input.value;
|
||||
this.tileMInput.value = settings.tile_m ?? this.tileMInput.value;
|
||||
this.gridStepInput.value = settings.grid_step ?? this.gridStepInput.value;
|
||||
this.dbCropSizesInput.value = settings.db_crop_sizes ?? this.dbCropSizesInput.value;
|
||||
this.droneSizeInput.value = settings.drone_size ?? this.droneSizeInput.value;
|
||||
this.queryFovInput.value = settings.query_fov_deg ?? this.queryFovInput.value;
|
||||
this.queryPitchInput.value = -90;
|
||||
this.height100Input.checked = settings.height_100 !== undefined ? !!settings.height_100 : true;
|
||||
this.height125Input.checked = settings.height_125 !== undefined ? !!settings.height_125 : true;
|
||||
this.height150Input.checked = settings.height_150 !== undefined ? !!settings.height_150 : true;
|
||||
this.height100ValueInput.value = settings.height_100_value ?? this.height100ValueInput.value;
|
||||
this.height125ValueInput.value = settings.height_125_value ?? this.height125ValueInput.value;
|
||||
this.height150ValueInput.value = settings.height_150_value ?? this.height150ValueInput.value;
|
||||
this.heightAbsoluteInput.checked = settings.height_absolute !== undefined ? !!settings.height_absolute : true;
|
||||
this.preloadInput.checked = settings.preload !== undefined ? !!settings.preload : true;
|
||||
this.skipEmptyInput.checked = !!settings.skip_empty;
|
||||
|
||||
this.collectSettings();
|
||||
this.updateInfo();
|
||||
this.log('Настройки загружены', 'info');
|
||||
} catch (error) {
|
||||
this.log('Не удалось загрузить сохраненные настройки', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
try {
|
||||
this.collectSettings();
|
||||
this.updateInfo();
|
||||
const response = await fetch('/api/save-settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.settings)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.log('Настройки сохранены', 'success');
|
||||
this.updateStatus('Настройки сохранены', '');
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`Ошибка сохранения: ${error.message}`, 'error');
|
||||
this.updateStatus('Ошибка', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async createFolders() {
|
||||
try {
|
||||
this.collectSettings();
|
||||
this.updateStatus('Создание папок...', 'working');
|
||||
this.log('Создание папок...', 'info');
|
||||
const response = await fetch('/api/create-folders', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.settings)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.folders = result.folders;
|
||||
this.renderFolders();
|
||||
if (result.gridRows && result.gridCols) {
|
||||
this.log(`Сетка: ${result.gridCols}x${result.gridRows} (cols×rows)`, 'info');
|
||||
}
|
||||
this.log(`Создано папок: ${result.folders.length}`, 'success');
|
||||
this.updateStatus('Папки созданы', '');
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`Ошибка создания папок: ${error.message}`, 'error');
|
||||
this.updateStatus('Ошибка', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
renderFolders() {
|
||||
if (this.folders.length === 0) {
|
||||
this.foldersList.innerHTML = '<p class="empty-state">Папки еще не созданы</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
this.foldersList.innerHTML = this.folders.map(folder => `
|
||||
<div class="folder-item" title="${folder.path}">
|
||||
<div class="folder-icon">📁</div>
|
||||
<div class="folder-name">${folder.name}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async startCapture() {
|
||||
try {
|
||||
this.collectSettings();
|
||||
this.updateStatus('Захват изображений...', 'working');
|
||||
this.log('Запуск захвата...', 'info');
|
||||
const response = await fetch('/api/start-capture', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.settings)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
this.log(`Захват запущен. URL: ${result.url}`, 'success');
|
||||
this.updateStatus('Захват запущен', '');
|
||||
window.open(result.url, '_blank', 'width=900,height=900');
|
||||
} else {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.log(`Ошибка запуска захвата: ${error.message}`, 'error');
|
||||
this.updateStatus('Ошибка', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
window.sandCastleUI = new SandCastleUI();
|
||||
console.log('SandCastle UI initialized');
|
||||
});
|
||||
Reference in New Issue
Block a user