2D-карта (станции, заявки) и вкладка «Заявки»
Новая вкладка «Заявки»: показ заявок за интервал на 2D-карте, создание заявки рисованием полигона на карте с формой параметров (Оптика/РСА) и удаление выбранной заявки (createRequest/deleteRequest, RequestCreatePanel/RequestDetailsPanel, polygonToWkt). В Tgu2DMapView добавлен изолированный режим рисования полигона. 2D-карта: панели информации о станции и о заявках, слой станций (stationsApi), поддержка выбора объектов через спатиальный индекс. Список слоёв упрощён — трасса/полоса/маркеры КА убраны из переключателя, по умолчанию включены работы плана, станции и заявки.
This commit is contained in:
@@ -7,9 +7,9 @@ import { drawSwath } from "./canvas/drawSwath";
|
||||
import { drawTiles } from "./canvas/drawTiles";
|
||||
import { drawTracks } from "./canvas/drawTracks";
|
||||
import { createMapProjection } from "./geometry/mapProjection";
|
||||
import { createPolygonSpatialIndex, hitTestPolygons, queryPolygonsByBBox } from "./geometry/spatialIndex";
|
||||
import { createPolygonSpatialIndex, hitTestAllPolygons, hitTestPolygons, queryPolygonsByBBox } from "./geometry/spatialIndex";
|
||||
import type { Tgu2DMapLayersState } from "./model/mapLayerTypes";
|
||||
import type { MapPolygon, MapSize, MapViewState, ScreenPoint, Tgu2DMapScene } from "./model/mapTypes";
|
||||
import type { GeoPoint, MapMarker, MapPolygon, MapSize, MapViewState, ScreenPoint, Tgu2DMapScene } from "./model/mapTypes";
|
||||
import type { Tgu2DMapSelection } from "./model/mapSelectionTypes";
|
||||
|
||||
type Tgu2DMapViewProps = {
|
||||
@@ -17,12 +17,18 @@ type Tgu2DMapViewProps = {
|
||||
layers: Tgu2DMapLayersState;
|
||||
redrawToken: number;
|
||||
onObjectSelect: (selection?: Tgu2DMapSelection) => void;
|
||||
onRequestsAtPoint?: (polygons: MapPolygon[]) => void;
|
||||
hoveredRequestPolygonId?: string;
|
||||
selectedRequestPolygonId?: string;
|
||||
pickMode?: boolean;
|
||||
onPickPoint?: (point: { lat: number; lon: number }) => void;
|
||||
targets?: Tgu2DMapTarget[];
|
||||
pickTarget?: Tgu2DMapTarget;
|
||||
selectedTargetId?: string;
|
||||
onTargetSelect?: (id: string) => void;
|
||||
drawMode?: boolean;
|
||||
draftPolygon?: GeoPoint[];
|
||||
onDrawAddPoint?: (point: GeoPoint) => void;
|
||||
};
|
||||
|
||||
export type Tgu2DMapTarget = {
|
||||
@@ -59,17 +65,26 @@ export function Tgu2DMapView({
|
||||
layers,
|
||||
redrawToken,
|
||||
onObjectSelect,
|
||||
onRequestsAtPoint,
|
||||
hoveredRequestPolygonId,
|
||||
selectedRequestPolygonId,
|
||||
pickMode = false,
|
||||
onPickPoint,
|
||||
targets = [],
|
||||
pickTarget,
|
||||
selectedTargetId,
|
||||
onTargetSelect
|
||||
onTargetSelect,
|
||||
drawMode = false,
|
||||
draftPolygon = [],
|
||||
onDrawAddPoint
|
||||
}: Tgu2DMapViewProps) {
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const dragRef = useRef<DragState | undefined>(undefined);
|
||||
const hasDraggedRef = useRef(false);
|
||||
const hoverRef = useRef<MapPolygon | undefined>(undefined);
|
||||
const hoverMarkerRef = useRef<MapMarker | undefined>(undefined);
|
||||
const selectedMarkerRef = useRef<MapMarker | undefined>(undefined);
|
||||
const [size, setSize] = useState<MapSize>({ width: 1000, height: 560 });
|
||||
const [view, setView] = useState<MapViewState>(INITIAL_VIEW);
|
||||
const [tileVersion, setTileVersion] = useState(0);
|
||||
@@ -81,6 +96,10 @@ export function Tgu2DMapView({
|
||||
[scene.polygons, layers]
|
||||
);
|
||||
const polygonIndex = useMemo(() => createPolygonSpatialIndex(activePolygons), [activePolygons]);
|
||||
const activeMarkers = useMemo(
|
||||
() => scene.markers.filter((marker) => layers[marker.layer]),
|
||||
[scene.markers, layers]
|
||||
);
|
||||
const projection = useMemo(() => createMapProjection(view, size), [view, size]);
|
||||
const visiblePolygons = useMemo(() => {
|
||||
// When the canvas is wider than the world tile, the entire globe is visible —
|
||||
@@ -170,11 +189,15 @@ export function Tgu2DMapView({
|
||||
const updateHover = useCallback(
|
||||
(event: React.PointerEvent) => {
|
||||
const screenPoint = screenPointFromEvent(event);
|
||||
const polygon = hitTestPolygons(polygonIndex, projection.unproject(screenPoint));
|
||||
if (polygon?.id === hoverRef.current?.id) return;
|
||||
const marker = hitTestMarkers(activeMarkers, screenPoint, projection);
|
||||
const polygon = marker ? undefined : hitTestPolygons(polygonIndex, projection.unproject(screenPoint));
|
||||
|
||||
if (marker?.id === hoverMarkerRef.current?.id && polygon?.id === hoverRef.current?.id) return;
|
||||
|
||||
hoverMarkerRef.current = marker;
|
||||
hoverRef.current = polygon;
|
||||
if (!polygon) {
|
||||
|
||||
if (!marker && !polygon) {
|
||||
setTooltip(undefined);
|
||||
return;
|
||||
}
|
||||
@@ -182,15 +205,16 @@ export function Tgu2DMapView({
|
||||
setTooltip({
|
||||
x: screenPoint.x + 12,
|
||||
y: screenPoint.y + 12,
|
||||
title: polygon.name,
|
||||
subtitle: layerTitle(polygon.layer)
|
||||
title: marker ? marker.label : polygon!.name,
|
||||
subtitle: marker ? "Станция" : layerTitle(polygon!.layer)
|
||||
});
|
||||
},
|
||||
[polygonIndex, projection, screenPointFromEvent]
|
||||
[activeMarkers, polygonIndex, projection, screenPointFromEvent]
|
||||
);
|
||||
|
||||
const onPointerDown = (event: React.PointerEvent) => {
|
||||
const screenPoint = screenPointFromEvent(event);
|
||||
hasDraggedRef.current = false;
|
||||
dragRef.current = {
|
||||
startX: screenPoint.x,
|
||||
startY: screenPoint.y,
|
||||
@@ -199,6 +223,8 @@ export function Tgu2DMapView({
|
||||
wrapperRef.current?.setPointerCapture(event.pointerId);
|
||||
};
|
||||
|
||||
const DRAG_THRESHOLD = 4;
|
||||
|
||||
const onPointerMove = (event: React.PointerEvent) => {
|
||||
const drag = dragRef.current;
|
||||
if (!drag) {
|
||||
@@ -209,6 +235,9 @@ export function Tgu2DMapView({
|
||||
const screenPoint = screenPointFromEvent(event);
|
||||
const dx = screenPoint.x - drag.startX;
|
||||
const dy = screenPoint.y - drag.startY;
|
||||
if (!hasDraggedRef.current && Math.sqrt(dx * dx + dy * dy) > DRAG_THRESHOLD) {
|
||||
hasDraggedRef.current = true;
|
||||
}
|
||||
const dragProjection = createMapProjection(
|
||||
{
|
||||
centerLon: drag.centerAtStart.lon,
|
||||
@@ -231,15 +260,53 @@ export function Tgu2DMapView({
|
||||
};
|
||||
|
||||
const onClick = (event: React.MouseEvent) => {
|
||||
if (hasDraggedRef.current) return;
|
||||
const screenPoint = screenPointFromEvent(event);
|
||||
if (drawMode) {
|
||||
onDrawAddPoint?.(projection.unproject(screenPoint));
|
||||
return;
|
||||
}
|
||||
if (pickMode) {
|
||||
onPickPoint?.(projection.unproject(screenPoint));
|
||||
onObjectSelect(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const polygon = hitTestPolygons(polygonIndex, projection.unproject(screenPoint));
|
||||
const marker = hitTestMarkers(activeMarkers, screenPoint, projection);
|
||||
if (marker) {
|
||||
selectedMarkerRef.current = marker;
|
||||
selectedPolygonRef.current = undefined;
|
||||
onRequestsAtPoint?.([]);
|
||||
onObjectSelect({
|
||||
type: "marker",
|
||||
id: marker.id,
|
||||
name: marker.label,
|
||||
layer: marker.layer,
|
||||
stationNumber: marker.stationNumber,
|
||||
lat: marker.point.lat,
|
||||
lon: marker.point.lon,
|
||||
altitude: marker.altitude,
|
||||
elevationMin: marker.elevationMin,
|
||||
elevationMax: marker.elevationMax
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const geoPoint = projection.unproject(screenPoint);
|
||||
const polygon = hitTestPolygons(polygonIndex, geoPoint);
|
||||
|
||||
if (polygon?.layer === "requests" && onRequestsAtPoint) {
|
||||
const allRequests = hitTestAllPolygons(polygonIndex, geoPoint).filter((p) => p.layer === "requests");
|
||||
onRequestsAtPoint(allRequests);
|
||||
selectedPolygonRef.current = undefined;
|
||||
selectedMarkerRef.current = undefined;
|
||||
onObjectSelect(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
onRequestsAtPoint?.([]);
|
||||
selectedPolygonRef.current = polygon;
|
||||
selectedMarkerRef.current = undefined;
|
||||
|
||||
if (!polygon) {
|
||||
onObjectSelect(undefined);
|
||||
@@ -276,11 +343,21 @@ export function Tgu2DMapView({
|
||||
};
|
||||
|
||||
const selectedPolygon = selectedPolygonRef.current;
|
||||
const overlayPolygons = uniquePolygons([selectedPolygon, hoverRef.current]);
|
||||
const selectedMarker = selectedMarkerRef.current;
|
||||
const externalSelectedPolygon = useMemo(
|
||||
() => selectedRequestPolygonId ? activePolygons.find((p) => p.id === selectedRequestPolygonId) : undefined,
|
||||
[selectedRequestPolygonId, activePolygons]
|
||||
);
|
||||
const externalHoveredPolygon = useMemo(
|
||||
() => hoveredRequestPolygonId ? activePolygons.find((p) => p.id === hoveredRequestPolygonId) : undefined,
|
||||
[hoveredRequestPolygonId, activePolygons]
|
||||
);
|
||||
const overlayPolygons = uniquePolygons([externalSelectedPolygon, selectedPolygon, externalHoveredPolygon, hoverRef.current]);
|
||||
const overlayMarkers = uniqueMarkers([selectedMarker, hoverMarkerRef.current]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`tgu-2d-map ${pickMode ? "is-picking" : ""}`}
|
||||
className={`tgu-2d-map ${pickMode || drawMode ? "is-picking" : ""}`}
|
||||
ref={wrapperRef}
|
||||
onClick={onClick}
|
||||
onPointerDown={onPointerDown}
|
||||
@@ -290,6 +367,7 @@ export function Tgu2DMapView({
|
||||
onPointerLeave={() => {
|
||||
dragRef.current = undefined;
|
||||
hoverRef.current = undefined;
|
||||
hoverMarkerRef.current = undefined;
|
||||
setTooltip(undefined);
|
||||
}}
|
||||
onWheel={onWheel}
|
||||
@@ -299,7 +377,10 @@ export function Tgu2DMapView({
|
||||
{overlayPolygons.flatMap((polygon) => {
|
||||
const raw = polygon.points.map((point) => projection.project(point));
|
||||
const pts = unwrapX(raw, projection.worldSize);
|
||||
const cls = polygon.id === selectedPolygon?.id ? "is-selected" : "is-hovered";
|
||||
const cls =
|
||||
polygon.id === selectedPolygon?.id || polygon.id === externalSelectedPolygon?.id
|
||||
? "is-selected"
|
||||
: "is-hovered";
|
||||
return ([0, projection.worldSize, -projection.worldSize] as const).flatMap((dx) => {
|
||||
const visible = pts.some((p) => {
|
||||
const x = p.x + dx;
|
||||
@@ -315,6 +396,11 @@ export function Tgu2DMapView({
|
||||
);
|
||||
});
|
||||
})}
|
||||
{overlayMarkers.map((marker) => {
|
||||
const pt = projection.project(marker.point);
|
||||
const cls = marker.id === selectedMarker?.id ? "is-selected" : "is-hovered";
|
||||
return <circle key={marker.id} cx={pt.x} cy={pt.y} r={10} className={cls} />;
|
||||
})}
|
||||
{targets.map((t) => {
|
||||
const screen = projection.project(t);
|
||||
const isSelected = t.id === selectedTargetId;
|
||||
@@ -344,6 +430,22 @@ export function Tgu2DMapView({
|
||||
</g>
|
||||
);
|
||||
})()}
|
||||
{draftPolygon.length > 0 && (() => {
|
||||
const pts = unwrapX(draftPolygon.map((point) => projection.project(point)), projection.worldSize);
|
||||
const path = pts.map((p) => `${p.x},${p.y}`).join(" ");
|
||||
return (
|
||||
<g className="tgu-2d-map__draft">
|
||||
{pts.length >= 2 && (
|
||||
pts.length >= 3
|
||||
? <polygon className="tgu-2d-map__draft-fill" points={path} />
|
||||
: <polyline className="tgu-2d-map__draft-line" points={path} />
|
||||
)}
|
||||
{pts.map((p, i) => (
|
||||
<circle key={i} cx={p.x} cy={p.y} r={4} className="tgu-2d-map__draft-vertex" />
|
||||
))}
|
||||
</g>
|
||||
);
|
||||
})()}
|
||||
</svg>
|
||||
{tooltip && (
|
||||
<div className="tgu-2d-map__tooltip" style={{ left: tooltip.x, top: tooltip.y }}>
|
||||
@@ -388,6 +490,36 @@ function unwrapX(points: { x: number; y: number }[], worldSize: number): { x: nu
|
||||
return out;
|
||||
}
|
||||
|
||||
function hitTestMarkers(
|
||||
markers: MapMarker[],
|
||||
screenPoint: ScreenPoint,
|
||||
projection: ReturnType<typeof createMapProjection>,
|
||||
hitRadius = 10
|
||||
): MapMarker | undefined {
|
||||
let best: MapMarker | undefined;
|
||||
let bestDist = hitRadius;
|
||||
for (const marker of markers) {
|
||||
const pt = projection.project(marker.point);
|
||||
const dist = Math.sqrt((pt.x - screenPoint.x) ** 2 + (pt.y - screenPoint.y) ** 2);
|
||||
if (dist <= bestDist) {
|
||||
bestDist = dist;
|
||||
best = marker;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
function uniqueMarkers(markers: Array<MapMarker | undefined>): MapMarker[] {
|
||||
const result: MapMarker[] = [];
|
||||
const ids = new Set<string>();
|
||||
for (const marker of markers) {
|
||||
if (!marker || ids.has(marker.id)) continue;
|
||||
ids.add(marker.id);
|
||||
result.push(marker);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function uniquePolygons(polygons: Array<MapPolygon | undefined>): MapPolygon[] {
|
||||
const result: MapPolygon[] = [];
|
||||
const ids = new Set<string>();
|
||||
|
||||
Reference in New Issue
Block a user