2025-12-19 13:24:56 +05:00
|
|
|
|
import type { MutableRefObject, RefObject } from "react";
|
|
|
|
|
|
import { FormEvent } from "react";
|
2025-12-19 14:11:32 +05:00
|
|
|
|
import type { AppConfig, SliceParams } from "../types";
|
2025-12-19 13:24:56 +05:00
|
|
|
|
|
|
|
|
|
|
type SettingsPanelProps = {
|
|
|
|
|
|
open: boolean;
|
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
|
config: AppConfig | null;
|
|
|
|
|
|
status: string;
|
|
|
|
|
|
theme: "dark" | "light";
|
|
|
|
|
|
onToggleTheme: () => void;
|
|
|
|
|
|
tilesets: string[];
|
|
|
|
|
|
activeTileset: string;
|
|
|
|
|
|
onActivateTileset: (name: string) => void;
|
|
|
|
|
|
onRefreshTilesets: () => void;
|
|
|
|
|
|
onCreateTileset: (name: string) => void;
|
|
|
|
|
|
sliceParams: SliceParams;
|
|
|
|
|
|
onSliceParamsChange: (params: SliceParams) => void;
|
|
|
|
|
|
sliceStatus: string;
|
|
|
|
|
|
onSliceSubmit: (event?: FormEvent<HTMLFormElement>) => void;
|
|
|
|
|
|
onSliceReset: () => void;
|
|
|
|
|
|
selectedFileName: string;
|
|
|
|
|
|
onTriggerFileSelect: () => void;
|
|
|
|
|
|
onFileSelected: (file: File | null | undefined) => void;
|
|
|
|
|
|
fileInputRef: RefObject<HTMLInputElement | null> | MutableRefObject<HTMLInputElement | null>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const SettingsPanel = ({
|
|
|
|
|
|
open,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
config,
|
|
|
|
|
|
status,
|
|
|
|
|
|
theme,
|
|
|
|
|
|
onToggleTheme,
|
|
|
|
|
|
tilesets,
|
|
|
|
|
|
activeTileset,
|
|
|
|
|
|
onActivateTileset,
|
|
|
|
|
|
onRefreshTilesets,
|
|
|
|
|
|
onCreateTileset,
|
|
|
|
|
|
sliceParams,
|
|
|
|
|
|
onSliceParamsChange,
|
|
|
|
|
|
sliceStatus,
|
|
|
|
|
|
onSliceSubmit,
|
|
|
|
|
|
onSliceReset,
|
|
|
|
|
|
selectedFileName,
|
|
|
|
|
|
onTriggerFileSelect,
|
|
|
|
|
|
onFileSelected,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
}: SettingsPanelProps) => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className={`settings-backdrop ${open ? "open" : ""}`} onClick={onClose} />
|
|
|
|
|
|
<aside className={`settings-panel ${open ? "open" : ""}`}>
|
|
|
|
|
|
<div className="settings-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="panel-title">Настройки</div>
|
|
|
|
|
|
<div className="panel-subtitle">Управление источниками и нарезкой</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="settings-content">
|
|
|
|
|
|
<section className="panel info">
|
|
|
|
|
|
<div className="info-grid">
|
|
|
|
|
|
<div className="card">
|
|
|
|
|
|
<div className="card-label">GNSS источник</div>
|
|
|
|
|
|
<div className="card-value">{config?.gnss_source ?? "—"}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="card">
|
|
|
|
|
|
<div className="card-label">Набор тайлов</div>
|
|
|
|
|
|
<div className="card-value">{activeTileset || "—"}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="card">
|
|
|
|
|
|
<div className="card-label">Статус</div>
|
|
|
|
|
|
<div className="card-value">{status}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<div className="panel-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="panel-title">Тема</div>
|
|
|
|
|
|
<div className="panel-subtitle">Светлая или тёмная</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button type="button" className="btn ghost small" onClick={onToggleTheme}>
|
|
|
|
|
|
{theme === "dark" ? "Включить светлую" : "Включить тёмную"}
|
|
|
|
|
|
</button>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<div className="panel-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="panel-title">Наборы тайлов</div>
|
|
|
|
|
|
<div className="panel-subtitle">Активация и обновление</div>
|
|
|
|
|
|
</div>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
</div>
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<div className="tileset-row stacked">
|
|
|
|
|
|
<select value={activeTileset} onChange={(e) => onActivateTileset(e.target.value)}>
|
|
|
|
|
|
{tilesets.map((t) => (
|
|
|
|
|
|
<option key={t} value={t}>
|
|
|
|
|
|
{t}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button type="button" className="btn ghost" onClick={onRefreshTilesets}>
|
|
|
|
|
|
Обновить
|
|
|
|
|
|
</button>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
</div>
|
2025-12-19 14:11:32 +05:00
|
|
|
|
|
|
|
|
|
|
<div className="panel-header">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<div className="panel-title">Нарезка тайлов</div>
|
|
|
|
|
|
<div className="panel-subtitle">Запускает gdal2tiles на бэкенде</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className={`badge ${sliceStatus === "ok" ? "ok" : sliceStatus === "idle" ? "" : "warn"}`}>
|
|
|
|
|
|
{sliceStatus}
|
|
|
|
|
|
</div>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
</div>
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<form
|
|
|
|
|
|
className="slice-grid"
|
|
|
|
|
|
onSubmit={(event) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
onSliceSubmit(event);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<label className="field">
|
|
|
|
|
|
<span>Источник (GeoTIFF/MBTiles)</span>
|
|
|
|
|
|
<div className="file-row">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="/path/to/input.tif"
|
|
|
|
|
|
value={sliceParams.source}
|
|
|
|
|
|
onChange={(e) => onSliceParamsChange({ ...sliceParams, source: e.target.value })}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button type="button" className="btn ghost" onClick={onTriggerFileSelect}>
|
|
|
|
|
|
Выбрать
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
accept=".tif,.tiff,.mbtiles,.jpg,.jpeg,.png"
|
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
|
style={{ display: "none" }}
|
|
|
|
|
|
onChange={(e) => onFileSelected(e.target.files?.[0])}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{selectedFileName && <small className="muted">Файл: {selectedFileName}</small>}
|
|
|
|
|
|
</label>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<label className="field">
|
|
|
|
|
|
<span>Схема тайлов</span>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={sliceParams.scheme}
|
|
|
|
|
|
onChange={(e) => onSliceParamsChange({ ...sliceParams, scheme: e.target.value as "xyz" | "tms" })}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="xyz">XYZ (Y сверху, Leaflet)</option>
|
|
|
|
|
|
<option value="tms">TMS (Y снизу)</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<label className="field">
|
|
|
|
|
|
<span>Формат исходника</span>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={sliceParams.source_format}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
onSliceParamsChange({
|
|
|
|
|
|
...sliceParams,
|
|
|
|
|
|
source_format: e.target.value as "geotiff" | "mbtiles" | "tms" | "xyz",
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="geotiff">GeoTIFF/растровый TIFF</option>
|
|
|
|
|
|
<option value="mbtiles">MBTiles (растровый)</option>
|
|
|
|
|
|
<option value="tms">Папка TMS</option>
|
|
|
|
|
|
<option value="xyz">Папка XYZ</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<label className="field">
|
|
|
|
|
|
<span>Tileset (имя папки)</span>
|
|
|
|
|
|
<div className="file-row">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
list="tilesets-list"
|
|
|
|
|
|
placeholder="например: moscow (пусто → активный)"
|
|
|
|
|
|
value={sliceParams.tileset}
|
|
|
|
|
|
onChange={(e) => onSliceParamsChange({ ...sliceParams, tileset: e.target.value })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="btn ghost"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const name = sliceParams.tileset.trim();
|
|
|
|
|
|
if (!name) return;
|
|
|
|
|
|
onCreateTileset(name);
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
Создать
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<datalist id="tilesets-list">
|
|
|
|
|
|
{tilesets.map((t) => (
|
|
|
|
|
|
<option key={t} value={t} />
|
|
|
|
|
|
))}
|
|
|
|
|
|
</datalist>
|
|
|
|
|
|
<small className="muted">Активный: {activeTileset || "—"}</small>
|
|
|
|
|
|
</label>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<label className="field">
|
|
|
|
|
|
<span>Папка тайлов (опц.)</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
placeholder="лучше не трогать (по умолчанию tileset)"
|
|
|
|
|
|
value={sliceParams.destination}
|
|
|
|
|
|
onChange={(e) => onSliceParamsChange({ ...sliceParams, destination: e.target.value })}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<label className="field">
|
|
|
|
|
|
<span>Min zoom</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
value={sliceParams.min_zoom}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
onSliceParamsChange({
|
|
|
|
|
|
...sliceParams,
|
|
|
|
|
|
min_zoom: Number(e.target.value),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<label className="field">
|
|
|
|
|
|
<span>Max zoom</span>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
value={sliceParams.max_zoom}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
onSliceParamsChange({
|
|
|
|
|
|
...sliceParams,
|
|
|
|
|
|
max_zoom: Number(e.target.value),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
required
|
|
|
|
|
|
/>
|
|
|
|
|
|
</label>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<label className="field">
|
|
|
|
|
|
<span>Формат тайлов (сейчас поддерживается PNG)</span>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={sliceParams.tile_format}
|
|
|
|
|
|
onChange={(e) => onSliceParamsChange({ ...sliceParams, tile_format: e.target.value })}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="png">png</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</label>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
|
2025-12-19 14:11:32 +05:00
|
|
|
|
<divz>
|
|
|
|
|
|
<div className="actions-row">
|
|
|
|
|
|
<button type="submit" className="btn primary">
|
|
|
|
|
|
Запустить
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" className="btn ghost" onClick={onSliceReset}>
|
|
|
|
|
|
Сброс
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</divz>
|
|
|
|
|
|
</form>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
</section>
|
2025-12-19 14:11:32 +05:00
|
|
|
|
|
|
|
|
|
|
<div className="bottom-settings">
|
|
|
|
|
|
<button type="button" className="btn primary block" onClick={onClose}>
|
|
|
|
|
|
Закрыть
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
2025-12-19 13:24:56 +05:00
|
|
|
|
</div>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|