2D-карта (станции, заявки) и вкладка «Заявки»
Новая вкладка «Заявки»: показ заявок за интервал на 2D-карте, создание заявки рисованием полигона на карте с формой параметров (Оптика/РСА) и удаление выбранной заявки (createRequest/deleteRequest, RequestCreatePanel/RequestDetailsPanel, polygonToWkt). В Tgu2DMapView добавлен изолированный режим рисования полигона. 2D-карта: панели информации о станции и о заявках, слой станций (stationsApi), поддержка выбора объектов через спатиальный индекс. Список слоёв упрощён — трасса/полоса/маркеры КА убраны из переключателя, по умолчанию включены работы плана, станции и заявки.
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
import { useState } from "react";
|
||||
import type {
|
||||
OpticsParams,
|
||||
OpticsResultType,
|
||||
Polarisation,
|
||||
RsaParams,
|
||||
RsaResultType
|
||||
} from "../../model/requestTypes";
|
||||
|
||||
export type RequestFormSubmit = {
|
||||
name: string;
|
||||
importance: number;
|
||||
beginDateTime: string;
|
||||
endDateTime: string;
|
||||
kpp: number[];
|
||||
highPriorityTransmit: boolean;
|
||||
optics?: OpticsParams;
|
||||
rsa?: RsaParams;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
vertexCount: number;
|
||||
drawing: boolean;
|
||||
defaultBegin: string;
|
||||
defaultEnd: string;
|
||||
submitting: boolean;
|
||||
error?: string;
|
||||
onUndoPoint: () => void;
|
||||
onClear: () => void;
|
||||
onFinishDrawing: () => void;
|
||||
onRestartDrawing: () => void;
|
||||
onSubmit: (form: RequestFormSubmit) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
const OPTICS_RESULT_TYPES: { value: OpticsResultType; label: string }[] = [
|
||||
{ value: "PANCHROMATIC", label: "Панхроматическая" },
|
||||
{ value: "MULTISPECTRAL", label: "Мультиспектральная" },
|
||||
{ value: "PANSHARPENING", label: "Паншарпенинг" }
|
||||
];
|
||||
|
||||
const RSA_RESULT_TYPES: { value: RsaResultType; label: string }[] = [
|
||||
{ value: "SPOTLIGHT", label: "Spotlight" },
|
||||
{ value: "STRIPMAP", label: "Stripmap" },
|
||||
{ value: "SCANSAR", label: "ScanSAR" }
|
||||
];
|
||||
|
||||
const POLARISATIONS: { value: Polarisation; label: string }[] = [
|
||||
{ value: "HH", label: "HH" },
|
||||
{ value: "VV", label: "VV" }
|
||||
];
|
||||
|
||||
export function RequestCreatePanel({
|
||||
vertexCount,
|
||||
drawing,
|
||||
defaultBegin,
|
||||
defaultEnd,
|
||||
submitting,
|
||||
error,
|
||||
onUndoPoint,
|
||||
onClear,
|
||||
onFinishDrawing,
|
||||
onRestartDrawing,
|
||||
onSubmit,
|
||||
onCancel
|
||||
}: Props) {
|
||||
const [name, setName] = useState("");
|
||||
const [importance, setImportance] = useState("1");
|
||||
const [begin, setBegin] = useState(defaultBegin);
|
||||
const [end, setEnd] = useState(defaultEnd);
|
||||
const [kpp, setKpp] = useState("");
|
||||
const [highPriority, setHighPriority] = useState(false);
|
||||
|
||||
const [opticsEnabled, setOpticsEnabled] = useState(true);
|
||||
const [opticsResultType, setOpticsResultType] = useState<OpticsResultType>("PANCHROMATIC");
|
||||
const [opticsResolution, setOpticsResolution] = useState("1");
|
||||
const [sunAngleMin, setSunAngleMin] = useState("10");
|
||||
const [sunAngleMax, setSunAngleMax] = useState("90");
|
||||
const [clouds, setClouds] = useState("100");
|
||||
|
||||
const [rsaEnabled, setRsaEnabled] = useState(false);
|
||||
const [rsaResultType, setRsaResultType] = useState<RsaResultType>("STRIPMAP");
|
||||
const [interferometry, setInterferometry] = useState(false);
|
||||
const [polarisation, setPolarisation] = useState<Polarisation>("HH");
|
||||
const [rsaResolution, setRsaResolution] = useState("1");
|
||||
|
||||
const importanceNum = Number(importance);
|
||||
const beginMs = new Date(begin).getTime();
|
||||
const endMs = new Date(end).getTime();
|
||||
|
||||
const kppList = parseKpp(kpp);
|
||||
const kppValid = kppList !== null;
|
||||
|
||||
const geometryReady = vertexCount >= 3 && !drawing;
|
||||
|
||||
const validationError = buildValidationError({
|
||||
geometryReady,
|
||||
drawing,
|
||||
vertexCount,
|
||||
name,
|
||||
importance,
|
||||
importanceNum,
|
||||
beginMs,
|
||||
endMs,
|
||||
kppValid,
|
||||
opticsEnabled,
|
||||
opticsResolution,
|
||||
rsaEnabled,
|
||||
rsaResolution
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (validationError || !kppList) return;
|
||||
const optics: OpticsParams | undefined = opticsEnabled
|
||||
? {
|
||||
resultType: opticsResultType,
|
||||
resolution: Number(opticsResolution),
|
||||
sunAngleMin: Number(sunAngleMin),
|
||||
sunAngleMax: Number(sunAngleMax),
|
||||
clouds: Number(clouds)
|
||||
}
|
||||
: undefined;
|
||||
const rsa: RsaParams | undefined = rsaEnabled
|
||||
? {
|
||||
resultType: rsaResultType,
|
||||
interferometry,
|
||||
polarisation,
|
||||
resolution: Number(rsaResolution)
|
||||
}
|
||||
: undefined;
|
||||
|
||||
onSubmit({
|
||||
name: name.trim(),
|
||||
importance: importanceNum,
|
||||
beginDateTime: begin,
|
||||
endDateTime: end,
|
||||
kpp: kppList,
|
||||
highPriorityTransmit: highPriority,
|
||||
optics,
|
||||
rsa
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="tgu-map-requests-panel tgu-requests-form">
|
||||
<div className="tgu-panel-heading">
|
||||
<span>Новая заявка</span>
|
||||
<button className="tgu-link-button" type="button" onClick={onCancel}>
|
||||
Отмена
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tgu-requests-form__body">
|
||||
<section className="tgu-requests-form__section">
|
||||
<div className="tgu-map-panel__label">Геометрия (полигон)</div>
|
||||
<div className="tgu-requests-form__hint">
|
||||
{drawing
|
||||
? "Кликайте по карте, чтобы добавить вершины. Нажмите «Завершить» (≥3 вершин)."
|
||||
: geometryReady
|
||||
? `Полигон задан: ${vertexCount} вершин.`
|
||||
: "Полигон не задан."}
|
||||
</div>
|
||||
<div className="tgu-requests-form__geo-status">Вершин: {vertexCount}</div>
|
||||
<div className="tgu-requests-form__actions">
|
||||
{drawing ? (
|
||||
<>
|
||||
<button className="tgu-button tgu-button--sm" type="button" onClick={onUndoPoint} disabled={vertexCount === 0}>
|
||||
Отменить точку
|
||||
</button>
|
||||
<button className="tgu-button tgu-button--sm" type="button" onClick={onClear} disabled={vertexCount === 0}>
|
||||
Очистить
|
||||
</button>
|
||||
<button
|
||||
className="tgu-button tgu-button--primary tgu-button--sm"
|
||||
type="button"
|
||||
onClick={onFinishDrawing}
|
||||
disabled={vertexCount < 3}
|
||||
>
|
||||
Завершить
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button className="tgu-button tgu-button--sm" type="button" onClick={onRestartDrawing}>
|
||||
Перерисовать
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="tgu-requests-form__section">
|
||||
<div className="tgu-map-panel__label">Параметры заявки</div>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Название</span>
|
||||
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="Название задания" />
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Важность</span>
|
||||
<input type="number" min="0" step="0.1" value={importance} onChange={(e) => setImportance(e.target.value)} />
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Начало</span>
|
||||
<input type="datetime-local" value={begin} onChange={(e) => setBegin(e.target.value)} />
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Конец</span>
|
||||
<input type="datetime-local" value={end} onChange={(e) => setEnd(e.target.value)} />
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>КПП</span>
|
||||
<input type="text" value={kpp} onChange={(e) => setKpp(e.target.value)} placeholder="напр. 1, 2, 5" />
|
||||
</label>
|
||||
<label className="tgu-requests-checkbox">
|
||||
<input type="checkbox" checked={highPriority} onChange={(e) => setHighPriority(e.target.checked)} />
|
||||
<span>Высокоприоритетная передача</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className="tgu-requests-form__section">
|
||||
<label className="tgu-requests-checkbox tgu-requests-form__block-toggle">
|
||||
<input type="checkbox" checked={opticsEnabled} onChange={(e) => setOpticsEnabled(e.target.checked)} />
|
||||
<span>Оптическая съёмка</span>
|
||||
</label>
|
||||
{opticsEnabled && (
|
||||
<div className="tgu-requests-form__block">
|
||||
<label className="tgu-requests-field">
|
||||
<span>Результат</span>
|
||||
<select value={opticsResultType} onChange={(e) => setOpticsResultType(e.target.value as OpticsResultType)}>
|
||||
{OPTICS_RESULT_TYPES.map((o) => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Разрешение, м</span>
|
||||
<input type="number" min="0.1" step="0.1" value={opticsResolution} onChange={(e) => setOpticsResolution(e.target.value)} />
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Высота Солнца мин, °</span>
|
||||
<input type="number" min="0" max="90" step="1" value={sunAngleMin} onChange={(e) => setSunAngleMin(e.target.value)} />
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Высота Солнца макс, °</span>
|
||||
<input type="number" min="0" max="90" step="1" value={sunAngleMax} onChange={(e) => setSunAngleMax(e.target.value)} />
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Облачность макс, %</span>
|
||||
<input type="number" min="0" max="100" step="1" value={clouds} onChange={(e) => setClouds(e.target.value)} />
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="tgu-requests-form__section">
|
||||
<label className="tgu-requests-checkbox tgu-requests-form__block-toggle">
|
||||
<input type="checkbox" checked={rsaEnabled} onChange={(e) => setRsaEnabled(e.target.checked)} />
|
||||
<span>Радиолокационная съёмка (РСА)</span>
|
||||
</label>
|
||||
{rsaEnabled && (
|
||||
<div className="tgu-requests-form__block">
|
||||
<label className="tgu-requests-field">
|
||||
<span>Тип аппаратуры</span>
|
||||
<select value={rsaResultType} onChange={(e) => setRsaResultType(e.target.value as RsaResultType)}>
|
||||
{RSA_RESULT_TYPES.map((o) => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Поляризация</span>
|
||||
<select value={polarisation} onChange={(e) => setPolarisation(e.target.value as Polarisation)}>
|
||||
{POLARISATIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="tgu-requests-field">
|
||||
<span>Разрешение, м</span>
|
||||
<input type="number" min="0.1" step="0.1" value={rsaResolution} onChange={(e) => setRsaResolution(e.target.value)} />
|
||||
</label>
|
||||
<label className="tgu-requests-checkbox">
|
||||
<input type="checkbox" checked={interferometry} onChange={(e) => setInterferometry(e.target.checked)} />
|
||||
<span>Интерферометрия</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{(validationError || error) && (
|
||||
<div className="tgu-alert tgu-alert--danger tgu-alert--compact">{error ?? validationError}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="tgu-requests-form__footer">
|
||||
<button
|
||||
className="tgu-button tgu-button--primary"
|
||||
type="button"
|
||||
disabled={submitting || validationError != null}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{submitting ? "Создание..." : "Создать заявку"}
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function parseKpp(raw: string): number[] | null {
|
||||
const trimmed = raw.trim();
|
||||
if (trimmed === "") return [];
|
||||
const parts = trimmed.split(/[,\s]+/).filter(Boolean);
|
||||
const result: number[] = [];
|
||||
for (const part of parts) {
|
||||
const n = Number(part);
|
||||
if (!Number.isInteger(n) || n < 0) return null;
|
||||
result.push(n);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function buildValidationError(input: {
|
||||
geometryReady: boolean;
|
||||
drawing: boolean;
|
||||
vertexCount: number;
|
||||
name: string;
|
||||
importance: string;
|
||||
importanceNum: number;
|
||||
beginMs: number;
|
||||
endMs: number;
|
||||
kppValid: boolean;
|
||||
opticsEnabled: boolean;
|
||||
opticsResolution: string;
|
||||
rsaEnabled: boolean;
|
||||
rsaResolution: string;
|
||||
}): string | null {
|
||||
if (input.drawing) return "Завершите рисование полигона (≥3 вершин).";
|
||||
if (!input.geometryReady) return "Задайте полигон минимум из 3 вершин.";
|
||||
if (input.name.trim() === "") return "Укажите название заявки.";
|
||||
if (input.importance.trim() === "" || !Number.isFinite(input.importanceNum) || input.importanceNum < 0) {
|
||||
return "Важность должна быть числом ≥ 0.";
|
||||
}
|
||||
if (!Number.isFinite(input.beginMs) || !Number.isFinite(input.endMs) || input.beginMs >= input.endMs) {
|
||||
return "Начало должно быть раньше конца.";
|
||||
}
|
||||
if (!input.kppValid) return "КПП — список целых чисел через запятую.";
|
||||
if (!input.opticsEnabled && !input.rsaEnabled) {
|
||||
return "Включите хотя бы один блок съёмки: Оптика или РСА.";
|
||||
}
|
||||
if (input.opticsEnabled && !(Number(input.opticsResolution) >= 0.1)) {
|
||||
return "Разрешение оптики должно быть ≥ 0.1 м.";
|
||||
}
|
||||
if (input.rsaEnabled && !(Number(input.rsaResolution) >= 0.1)) {
|
||||
return "Разрешение РСА должно быть ≥ 0.1 м.";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user