FEAT - FOUNDRY: activate sector workspace

This commit is contained in:
Codex
2026-08-08 14:09:51 +03:00
parent 30c9b8c5fd
commit 58800d9576
10 changed files with 675 additions and 80 deletions
+404 -67
View File
@@ -1,5 +1,5 @@
import { forwardRef, lazy, Suspense, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, type CSSProperties, type KeyboardEvent, type PointerEvent } from "react";
import { Button, Checker, ColorField, ControlRow, Dropdown, Icon, IconButton, Inspector, InspectorSelectField, RangeControl, SegmentedControl, Window, WorkspaceWindow } from "@nodedc/ui-react";
import { Button, Checker, ColorField, ControlRow, Dropdown, Icon, IconButton, Inspector, InspectorSelectField, RangeControl, SegmentedControl, WorkspaceWindow } from "@nodedc/ui-react";
import type { SelectOption, WorkspaceWindowRect } from "@nodedc/ui-react";
import type {
CameraSpiralState,
@@ -12,6 +12,7 @@ import type {
GridSectorSelection,
} from "./CesiumMapRenderer.js";
import { mapRuntimeEntityId, useMapDataProductRuntime } from "./useMapDataProductRuntime.js";
import type { MapRuntimeFact } from "./useMapDataProductRuntime.js";
import {
compareMapRuntimeFacts,
mapFactMatchesFilters,
@@ -48,6 +49,7 @@ import {
graticuleSectorAt,
graticuleSectorSummary,
localSectorAt,
localSectorAtGeodetic,
localSectorSummary,
localVolumeAt,
type GraticuleSectorAddress,
@@ -309,6 +311,50 @@ function gridSectorCenterLabel(selection: GridSectorSelection) {
: `${formatGridMetric(selection.center.latitude, 6)}°, ${formatGridMetric(selection.center.longitude, 6)}°`;
}
const MAP_SCOPE_PROVIDER_FIELD = "position_source";
const MAP_SCOPE_OBJECT_KIND_FIELD = "object_kind";
const MAP_SCOPE_MISSING_VALUE = "__nodedc_missing__";
function normalizedSectorScopeValue(value: unknown) {
if (typeof value !== "string") return null;
const normalized = value.trim();
if (!normalized || normalized.length > 120 || /[\u0000-\u001f\u007f]/.test(normalized)) return null;
return normalized;
}
function sectorScopeValueLabel(value: string) {
if (value === MAP_SCOPE_MISSING_VALUE) return "Не указано";
return value.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
}
function mapFactSectorScopeValue(fact: MapRuntimeFact, field: string) {
return normalizedSectorScopeValue(fact.attributes[field]) ?? MAP_SCOPE_MISSING_VALUE;
}
function mapFactInsideGridSector(
fact: MapRuntimeFact,
selection: GridSectorSelection,
profiles: SectorGridLodProfile[],
origin: { latitude: number; longitude: number },
) {
if (fact.geometry?.type !== "Point") return false;
const [longitude, latitude] = fact.geometry.coordinates;
const profile = profiles[selection.lod - 1];
if (!profile || profile.mode !== selection.mode) return false;
if (selection.mode === "graticule") {
return graticuleSectorAt({ longitude, latitude }, {
lod: selection.lod,
stepDegrees: profile.graticuleStepDegrees,
}).id === selection.id;
}
return localSectorAtGeodetic({ longitude, latitude }, {
lod: selection.lod,
originLatitude: origin.latitude,
originLongitude: origin.longitude,
stepMeters: profile.stepKm * 1_000,
}).id === selection.id;
}
function beginGatewayHealthEpoch(order: GatewayHealthOrder) {
order.nextEpoch += 1;
order.latestStartedEpoch = order.nextEpoch;
@@ -415,6 +461,8 @@ export type MapSubjectWindowState = {
zIndex: number;
};
type MapWorkspaceWindowId = "settings" | "layers" | "sector" | "subject-card" | `binding:${string}`;
export type MapSubjectState = {
bindingId: string;
visible: boolean;
@@ -594,17 +642,31 @@ function defaultSubjectWindowState(index: number): MapSubjectWindowState {
}
const defaultLayersWindowRect: WorkspaceWindowRect = {
x: 1024,
x: 24,
y: 72,
width: 336,
height: 500,
};
const defaultSettingsWindowRect: WorkspaceWindowRect = {
x: 930,
y: 72,
width: 420,
height: 530,
};
const defaultSectorWindowRect: WorkspaceWindowRect = {
x: 24,
y: 72,
width: 380,
height: 530,
};
const defaultSubjectCardRect: WorkspaceWindowRect = {
x: 940,
y: 52,
y: 72,
width: 390,
height: 560,
height: 520,
};
function initialSubjectState(bindings: MapDataProductBinding[], saved: MapSubjectState[] | undefined) {
@@ -665,10 +727,12 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
const [subjectCardRect, setSubjectCardRect] = useState<WorkspaceWindowRect>(defaultSubjectCardRect);
const [subjectCardMaximized, setSubjectCardMaximized] = useState(false);
const [subjectCardZIndex, setSubjectCardZIndex] = useState(140);
const [subjectCardActive, setSubjectCardActive] = useState(false);
const [subjectCardTabId, setSubjectCardTabId] = useState("overview");
const [expandedFacetRows, setExpandedFacetRows] = useState<Record<string, boolean>>({});
const [inspectorOpen, setInspectorOpen] = useState(false);
const [settingsWindowRect, setSettingsWindowRect] = useState<WorkspaceWindowRect>(defaultSettingsWindowRect);
const [settingsWindowMaximized, setSettingsWindowMaximized] = useState(false);
const [settingsWindowZIndex, setSettingsWindowZIndex] = useState(13);
const [inspectorOpenSections, setInspectorOpenSections] = useState<string[]>(() => (
initialLayout?.inspectorOpenSections ?? ["map-base"]
));
@@ -676,7 +740,14 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
const [layersWindowRect, setLayersWindowRect] = useState<WorkspaceWindowRect>(defaultLayersWindowRect);
const [layersWindowMaximized, setLayersWindowMaximized] = useState(false);
const [layersWindowZIndex, setLayersWindowZIndex] = useState(12);
const [layersWindowActive, setLayersWindowActive] = useState(false);
const [sectorWindowRect, setSectorWindowRect] = useState<WorkspaceWindowRect>(defaultSectorWindowRect);
const [sectorWindowMaximized, setSectorWindowMaximized] = useState(false);
const [sectorWindowZIndex, setSectorWindowZIndex] = useState(142);
const [hideObjectsOutsideSector, setHideObjectsOutsideSector] = useState(false);
const [sectorExcludedBindingIds, setSectorExcludedBindingIds] = useState<string[]>([]);
const [sectorExcludedProviders, setSectorExcludedProviders] = useState<string[]>([]);
const [sectorExcludedObjectKinds, setSectorExcludedObjectKinds] = useState<string[]>([]);
const [activeWorkspaceWindowId, setActiveWorkspaceWindowId] = useState<MapWorkspaceWindowId>();
const [toolbarOpen, setToolbarOpen] = useState(Boolean(features.toolbar));
const [searchOpen, setSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
@@ -734,7 +805,6 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
const [subjectStates, setSubjectStates] = useState<Record<string, MapSubjectState>>(() => (
initialSubjectState(initialLayout?.dataProductBindings ?? [], initialLayout?.subjectStates)
));
const [activeSubjectBindingId, setActiveSubjectBindingId] = useState<string>();
const presentationFilters = useMemo<MapPresentationFilters>(() => Object.fromEntries(
Object.entries(subjectStates).map(([bindingId, state]) => [bindingId, {
visible: state.visible,
@@ -765,6 +835,11 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
...presentationFilters,
...referencePresentationFilters,
}), [presentationFilters, referencePresentationFilters]);
const sectorGridLodProfiles = mapSettings.gridLodProfiles as SectorGridLodProfile[];
const fixedSectorGridOrigin = useMemo(() => ({
latitude: mapSettings.gridCenterLatitude,
longitude: mapSettings.gridCenterLongitude,
}), [mapSettings.gridCenterLatitude, mapSettings.gridCenterLongitude]);
const primaryBindingIds = useMemo(() => new Set(
dataProductBindings.filter((binding) => !binding.joinToBindingId).map((binding) => binding.id),
), [dataProductBindings]);
@@ -804,6 +879,77 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
});
})
), [dataProductBindings, presentationProfiles, primaryRuntimeBindings]);
const sectorSpatialEntities = useMemo(() => selectedGridSector
? selectable.filter((entity) => mapFactInsideGridSector(
entity.fact,
selectedGridSector,
sectorGridLodProfiles,
fixedSectorGridOrigin,
))
: [], [fixedSectorGridOrigin, sectorGridLodProfiles, selectable, selectedGridSector]);
const sectorBindingOptions = useMemo(() => [...dataProductBindings]
.filter((binding) => !binding.joinToBindingId)
.sort((left, right) => (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id))
.map((binding) => ({
value: binding.id,
label: binding.displayName?.trim() || binding.id,
count: sectorSpatialEntities.filter((entity) => entity.bindingId === binding.id).length,
})), [dataProductBindings, sectorSpatialEntities]);
const sectorProviderFacetAvailable = useMemo(() => dataProductBindings.some((binding) => (
!binding.joinToBindingId && binding.fieldProjection.includes(MAP_SCOPE_PROVIDER_FIELD)
)), [dataProductBindings]);
const sectorObjectKindFacetAvailable = useMemo(() => dataProductBindings.some((binding) => (
!binding.joinToBindingId && binding.fieldProjection.includes(MAP_SCOPE_OBJECT_KIND_FIELD)
)), [dataProductBindings]);
const sectorProviderOptions = useMemo(() => {
if (!sectorProviderFacetAvailable) return [];
const counts = new Map<string, number>();
sectorSpatialEntities.forEach(({ fact }) => {
const value = mapFactSectorScopeValue(fact, MAP_SCOPE_PROVIDER_FIELD);
counts.set(value, (counts.get(value) ?? 0) + 1);
});
return [...counts].map(([value, count]) => ({ value, count, label: sectorScopeValueLabel(value) }))
.sort((left, right) => left.label.localeCompare(right.label, "ru"));
}, [sectorProviderFacetAvailable, sectorSpatialEntities]);
const sectorObjectKindOptions = useMemo(() => {
if (!sectorObjectKindFacetAvailable) return [];
const counts = new Map<string, number>();
sectorSpatialEntities.forEach(({ fact }) => {
const value = mapFactSectorScopeValue(fact, MAP_SCOPE_OBJECT_KIND_FIELD);
counts.set(value, (counts.get(value) ?? 0) + 1);
});
return [...counts].map(([value, count]) => ({ value, count, label: sectorScopeValueLabel(value) }))
.sort((left, right) => left.label.localeCompare(right.label, "ru"));
}, [sectorObjectKindFacetAvailable, sectorSpatialEntities]);
const sectorVisibleEntities = useMemo(() => sectorSpatialEntities.filter((entity) => {
if (sectorExcludedBindingIds.includes(entity.bindingId)) return false;
if (sectorExcludedProviders.includes(mapFactSectorScopeValue(entity.fact, MAP_SCOPE_PROVIDER_FIELD))) return false;
if (sectorExcludedObjectKinds.includes(mapFactSectorScopeValue(entity.fact, MAP_SCOPE_OBJECT_KIND_FIELD))) return false;
const binding = dataProductBindings.find((candidate) => candidate.id === entity.bindingId);
const profile = mapPresentationProfileForFact(
presentationProfiles,
binding?.presentationProfileId,
entity.fact.semanticType,
);
return Boolean(profile && mapFactMatchesFilters(entity.fact, profile, presentationFilters, entity.bindingId));
}), [dataProductBindings, presentationFilters, presentationProfiles, sectorExcludedBindingIds, sectorExcludedObjectKinds, sectorExcludedProviders, sectorSpatialEntities]);
const sectorScopedPrimaryRuntimeBindings = useMemo(() => {
if (!selectedGridSector) return primaryRuntimeBindings;
return primaryRuntimeBindings.map((binding) => ({
...binding,
facts: binding.facts.filter((fact) => {
if (sectorExcludedBindingIds.includes(binding.bindingId)) return false;
if (sectorExcludedProviders.includes(mapFactSectorScopeValue(fact, MAP_SCOPE_PROVIDER_FIELD))) return false;
if (sectorExcludedObjectKinds.includes(mapFactSectorScopeValue(fact, MAP_SCOPE_OBJECT_KIND_FIELD))) return false;
return !hideObjectsOutsideSector || mapFactInsideGridSector(
fact,
selectedGridSector,
sectorGridLodProfiles,
fixedSectorGridOrigin,
);
}),
}));
}, [fixedSectorGridOrigin, hideObjectsOutsideSector, primaryRuntimeBindings, sectorExcludedBindingIds, sectorExcludedObjectKinds, sectorExcludedProviders, sectorGridLodProfiles, selectedGridSector]);
const presentationSummaries = useMemo(() => [...dataProductBindings]
.filter((binding) => !binding.joinToBindingId)
.sort((left, right) => (left.order ?? 0) - (right.order ?? 0) || left.id.localeCompare(right.id))
@@ -832,7 +978,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
}];
}), [presentationProfiles, referenceLayers, referenceRuntimeBindings]);
const objectLayerCount = presentationSummaries.length + referenceObjectSummaries.length;
const filteredTargets = useMemo(() => primaryRuntimeBindings.flatMap((binding) => {
const filteredTargets = useMemo(() => sectorScopedPrimaryRuntimeBindings.flatMap((binding) => {
const bindingConfig = dataProductBindings.find((candidate) => candidate.id === binding.bindingId);
return binding.facts.flatMap((fact) => {
const profile = mapPresentationProfileForFact(
@@ -850,7 +996,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
renderable: mapRuntimeFactIsRenderable(fact, profile),
}];
});
}).sort((left, right) => left.title.localeCompare(right.title, "ru")), [dataProductBindings, presentationFilters, presentationProfiles, primaryRuntimeBindings]);
}).sort((left, right) => left.title.localeCompare(right.title, "ru")), [dataProductBindings, presentationFilters, presentationProfiles, sectorScopedPrimaryRuntimeBindings]);
const visibleTargetEntityIds = useMemo(() => (
filteredTargets.filter((target) => target.renderable).map((target) => target.entityId)
), [filteredTargets]);
@@ -927,7 +1073,6 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
const selectedGridLodIndex = Math.max(0, Math.min(4, Number.parseInt(selectedGridLod, 10) || 0));
const activeGridLod = (mapSettings.gridLodProfiles[selectedGridLodIndex]
?? DEFAULT_GRID_LOD_PROFILES[selectedGridLodIndex]) as SectorGridLodProfile;
const sectorGridLodProfiles = mapSettings.gridLodProfiles as SectorGridLodProfile[];
const gridSectorDefinitionKey = useMemo(() => JSON.stringify({
origin: [mapSettings.gridCenterLatitude, mapSettings.gridCenterLongitude],
profiles: sectorGridLodProfiles.map((profile) => ({
@@ -942,10 +1087,6 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
volumeBandHeightMeters: profile.volumeBandHeightMeters,
})),
}), [mapSettings.gridCenterLatitude, mapSettings.gridCenterLongitude, sectorGridLodProfiles]);
const fixedSectorGridOrigin = useMemo(() => ({
latitude: mapSettings.gridCenterLatitude,
longitude: mapSettings.gridCenterLongitude,
}), [mapSettings.gridCenterLatitude, mapSettings.gridCenterLongitude]);
const selectedGridParentLod = useMemo(() => selectedGridSector
? gridSectorParentLodSelection(selectedGridSector, sectorGridLodProfiles, fixedSectorGridOrigin)
: null, [fixedSectorGridOrigin, sectorGridLodProfiles, selectedGridSector]);
@@ -971,6 +1112,10 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
useEffect(() => {
setSelectedGridSector(null);
setSectorExcludedBindingIds([]);
setSectorExcludedProviders([]);
setSectorExcludedObjectKinds([]);
setActiveWorkspaceWindowId((current) => current === "sector" ? undefined : current);
}, [gridSectorDefinitionKey]);
const minimumGridLodHeight = selectedGridLodIndex === 0
? 0.1
@@ -1208,7 +1353,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
}),
}), [dataProductBindings, inspectorOpenSections, mapCamera, mapHeight, mapSettings, pinBindings, presentationProfiles, presentationSummaries, referenceLayers, subjectDetailProfiles, subjectStates]);
const updateSubjectState = (bindingId: string, update: (state: MapSubjectState) => MapSubjectState) => {
const updateSubjectState = useCallback((bindingId: string, update: (state: MapSubjectState) => MapSubjectState) => {
setSubjectStates((current) => {
const index = dataProductBindings.findIndex((binding) => binding.id === bindingId);
const state = current[bindingId] ?? {
@@ -1219,7 +1364,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
};
return { ...current, [bindingId]: update(state) };
});
};
}, [dataProductBindings]);
const togglePresentationFilter = (bindingId: string, field: string, value: string) => {
updateSubjectState(bindingId, (state) => {
@@ -1236,40 +1381,101 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
updateSubjectState(bindingId, (state) => ({ ...state, visible: !state.visible }));
};
const activateWorkspaceWindow = useCallback((windowId: MapWorkspaceWindowId) => {
if (activeWorkspaceWindowId === windowId) return;
const nextZIndex = Math.max(
20,
settingsWindowZIndex,
layersWindowZIndex,
sectorWindowZIndex,
subjectCardZIndex,
...Object.values(subjectStates).map((state) => state.window.zIndex),
) + 1;
if (windowId === "settings") setSettingsWindowZIndex(nextZIndex);
else if (windowId === "layers") setLayersWindowZIndex(nextZIndex);
else if (windowId === "sector") setSectorWindowZIndex(nextZIndex);
else if (windowId === "subject-card") setSubjectCardZIndex(nextZIndex);
else if (windowId.startsWith("binding:")) {
const bindingId = windowId.slice("binding:".length);
updateSubjectState(bindingId, (state) => ({
...state,
window: { ...state.window, zIndex: nextZIndex },
}));
}
setActiveWorkspaceWindowId(windowId);
}, [activeWorkspaceWindowId, layersWindowZIndex, sectorWindowZIndex, settingsWindowZIndex, subjectCardZIndex, subjectStates, updateSubjectState]);
const clearActiveWorkspaceWindow = (windowId: MapWorkspaceWindowId) => {
setActiveWorkspaceWindowId((current) => current === windowId ? undefined : current);
};
const openSubjectWindow = (bindingId: string) => {
const nextZIndex = Math.max(20, layersWindowZIndex, subjectCardZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
updateSubjectState(bindingId, (state) => ({
...state,
window: { ...state.window, open: true, zIndex: nextZIndex },
window: { ...state.window, open: true },
}));
setLayersWindowActive(false);
setSubjectCardActive(false);
setActiveSubjectBindingId(bindingId);
activateWorkspaceWindow(`binding:${bindingId}`);
};
const closeSubjectWindow = (bindingId: string) => {
updateSubjectState(bindingId, (state) => ({ ...state, window: { ...state.window, open: false } }));
setActiveSubjectBindingId((current) => current === bindingId ? undefined : current);
clearActiveWorkspaceWindow(`binding:${bindingId}`);
};
const activateLayersWindow = () => {
const nextZIndex = Math.max(20, layersWindowZIndex, subjectCardZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
setLayersWindowZIndex(nextZIndex);
setLayersWindowActive(true);
setSubjectCardActive(false);
setActiveSubjectBindingId(undefined);
activateWorkspaceWindow("layers");
};
const toggleLayersWindow = () => {
if (layersOpen) {
setLayersOpen(false);
setLayersWindowActive(false);
clearActiveWorkspaceWindow("layers");
return;
}
setLayersOpen(true);
activateLayersWindow();
};
const openSettingsWindow = () => {
setInspectorOpen(true);
activateWorkspaceWindow("settings");
};
const deactivateGridSector = () => {
setSelectedGridSector(null);
setSectorExcludedBindingIds([]);
setSectorExcludedProviders([]);
setSectorExcludedObjectKinds([]);
clearActiveWorkspaceWindow("sector");
};
const handleGridSectorSelect = (selection: GridSectorSelection | null) => {
if (!selection) {
deactivateGridSector();
return;
}
setSelectedGridSector(selection);
activateWorkspaceWindow("sector");
};
const setSectorBindingEnabled = (bindingId: string, enabled: boolean) => {
setSectorExcludedBindingIds((current) => enabled
? current.filter((value) => value !== bindingId)
: [...new Set([...current, bindingId])]);
};
const setSectorProviderEnabled = (provider: string, enabled: boolean) => {
setSectorExcludedProviders((current) => enabled
? current.filter((value) => value !== provider)
: [...new Set([...current, provider])]);
};
const setSectorObjectKindEnabled = (objectKind: string, enabled: boolean) => {
setSectorExcludedObjectKinds((current) => enabled
? current.filter((value) => value !== objectKind)
: [...new Set([...current, objectKind])]);
};
const updatePresentationProfile = (
profileId: string,
update: (profile: MapPresentationProfile) => MapPresentationProfile,
@@ -1297,11 +1503,8 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
: (profile?.defaultTabId ?? "overview")
));
setSubjectCardOpen(true);
setSubjectCardActive(true);
setLayersWindowActive(false);
setActiveSubjectBindingId(undefined);
setSubjectCardZIndex((current) => Math.max(current, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1);
}, [dataProductBindings, layersWindowZIndex, selectable, subjectDetailProfiles, subjectStates]);
activateWorkspaceWindow("subject-card");
}, [activateWorkspaceWindow, dataProductBindings, selectable, subjectDetailProfiles]);
const handleSelectAndFocus = useCallback((entityId: string) => {
handleSelect(entityId);
@@ -2027,7 +2230,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
key={rendererRevision}
ref={mapRendererRef}
onSelect={handleSelect}
onGridSectorSelect={setSelectedGridSector}
onGridSectorSelect={handleGridSectorSelect}
selectedGridSector={selectedGridSector}
onGatewayHealth={handleRendererGatewayHealth}
onProviderStatus={setProviderStatus}
@@ -2037,14 +2240,14 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
onSpiralStateChange={handleSpiralStateChange}
initialCamera={mapCamera ?? undefined}
presentation={presentation}
runtimeBindings={[...primaryRuntimeBindings, ...referenceRuntimeBindings]}
runtimeBindings={[...sectorScopedPrimaryRuntimeBindings, ...referenceRuntimeBindings]}
presentationProfiles={presentationProfiles}
presentationFilters={rendererPresentationFilters}
/>
</Suspense>
<div className="catalog-map-fixture__actions">
<IconButton label="Настройки карты" aria-pressed={inspectorOpen} data-active={inspectorOpen || undefined} onClick={() => setInspectorOpen(true)}><Icon name="settings" /></IconButton>
<IconButton label="Настройки карты" aria-pressed={inspectorOpen} data-active={inspectorOpen || undefined} onClick={openSettingsWindow}><Icon name="settings" /></IconButton>
<IconButton label="Слои карты" aria-pressed={layersOpen} data-active={layersOpen || undefined} onClick={toggleLayersWindow}><Icon name="grid" /></IconButton>
{features.toolbar ? <IconButton label="Toolbar" aria-pressed={toolbarOpen} data-active={toolbarOpen || undefined} onClick={() => setToolbarOpen((value) => !value)}><Icon name="panel" /></IconButton> : null}
{features.assistant ? <IconButton label="Assistant" aria-pressed={assistantOpen} data-active={assistantOpen || undefined} onClick={() => setAssistantOpen((value) => !value)}><Icon name="apps" /></IconButton> : null}
@@ -2060,10 +2263,10 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
onActivate={activateLayersWindow}
onClose={() => {
setLayersOpen(false);
setLayersWindowActive(false);
clearActiveWorkspaceWindow("layers");
}}
title="Слои карты"
active={layersWindowActive}
active={activeWorkspaceWindowId === "layers"}
zIndex={layersWindowZIndex}
minWidth={320}
minHeight={360}
@@ -2089,6 +2292,136 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
</WorkspaceWindow>
) : null}
{selectedGridSector ? (
<WorkspaceWindow
boundsRef={workspaceRef}
rect={sectorWindowRect}
onRectChange={setSectorWindowRect}
maximized={sectorWindowMaximized}
onMaximizedChange={setSectorWindowMaximized}
onActivate={() => activateWorkspaceWindow("sector")}
onClose={deactivateGridSector}
title={`Активный сектор · LOD ${selectedGridSector.lod}`}
subtitle={selectedGridSector.label}
status={`${sectorVisibleEntities.length} / ${sectorSpatialEntities.length}`}
active={activeWorkspaceWindowId === "sector"}
zIndex={sectorWindowZIndex}
minWidth={340}
minHeight={380}
footer={(
<Button variant="secondary" size="compact" width="full" onClick={deactivateGridSector}>
Деактивировать сектор
</Button>
)}
className="catalog-map-fixture__sector-window catalog-map-fixture__map-glass-window"
aria-label={`Активный сектор: ${selectedGridSector.id}`}
>
<div className="catalog-map-sector-window">
<section className="catalog-map-sector-window__summary" aria-label="Параметры сектора">
<code title={selectedGridSector.id}>{selectedGridSector.id}</code>
<span>{gridSectorBoundsLabel(selectedGridSector)}</span>
<span>{formatGridSectorArea(selectedGridSector.areaSquareMeters)}</span>
<Button
variant="secondary"
size="compact"
width="full"
icon={<Icon name={gridSectorCopyState === "copied" ? "check" : "copy"} />}
onClick={() => void copySelectedGridSectorId()}
>{gridSectorCopyState === "copied" ? "ID скопирован" : "Копировать stable ID"}</Button>
</section>
<Checker
checked={hideObjectsOutsideSector}
label="Скрыть объекты за сектором"
onChange={setHideObjectsOutsideSector}
/>
<section className="catalog-map-sector-window__filters" aria-labelledby="map-sector-domains-title">
<div className="catalog-map-sector-window__section-title">
<strong id="map-sector-domains-title">Домены данных</strong>
<small>{sectorBindingOptions.length}</small>
</div>
{sectorBindingOptions.map((option) => (
<Checker
key={option.value}
checked={!sectorExcludedBindingIds.includes(option.value)}
label={`${option.label} · ${option.count}`}
onChange={(enabled) => setSectorBindingEnabled(option.value, enabled)}
/>
))}
</section>
{sectorProviderOptions.length ? (
<section className="catalog-map-sector-window__filters" aria-labelledby="map-sector-providers-title">
<div className="catalog-map-sector-window__section-title">
<strong id="map-sector-providers-title">Провайдеры</strong>
<small>{sectorProviderOptions.length}</small>
</div>
{sectorProviderOptions.map((option) => (
<Checker
key={option.value}
checked={!sectorExcludedProviders.includes(option.value)}
label={`${option.label} · ${option.count}`}
title={option.value === MAP_SCOPE_MISSING_VALUE ? undefined : option.value}
onChange={(enabled) => setSectorProviderEnabled(option.value, enabled)}
/>
))}
</section>
) : null}
{sectorObjectKindOptions.length ? (
<section className="catalog-map-sector-window__filters" aria-labelledby="map-sector-kinds-title">
<div className="catalog-map-sector-window__section-title">
<strong id="map-sector-kinds-title">Типы объектов</strong>
<small>{sectorObjectKindOptions.length}</small>
</div>
{sectorObjectKindOptions.map((option) => (
<Checker
key={option.value}
checked={!sectorExcludedObjectKinds.includes(option.value)}
label={`${option.label} · ${option.count}`}
title={option.value === MAP_SCOPE_MISSING_VALUE ? undefined : option.value}
onChange={(enabled) => setSectorObjectKindEnabled(option.value, enabled)}
/>
))}
</section>
) : null}
<section className="catalog-map-sector-window__objects" aria-labelledby="map-sector-objects-title">
<div className="catalog-map-sector-window__section-title">
<strong id="map-sector-objects-title">Объекты сектора</strong>
<small>{sectorVisibleEntities.length} / {sectorSpatialEntities.length}</small>
</div>
<div className="catalog-map-sector-window__object-list">
{sectorVisibleEntities.map((entity) => {
const provider = normalizedSectorScopeValue(entity.fact.attributes[MAP_SCOPE_PROVIDER_FIELD]);
const objectKind = normalizedSectorScopeValue(entity.fact.attributes[MAP_SCOPE_OBJECT_KIND_FIELD]);
return (
<button
type="button"
key={entity.id}
data-selected={entity.id === selectedId || undefined}
onClick={() => handleSelectAndFocus(entity.id)}
>
<span>{entity.title}</span>
<code>{entity.fact.sourceId}</code>
<small>{[provider, objectKind, entity.status].filter(Boolean).join(" · ")}</small>
</button>
);
})}
{!sectorVisibleEntities.length ? (
<small className="catalog-map-sector-window__empty">
{sectorSpatialEntities.length
? "Объекты скрыты текущими фильтрами."
: "В секторе нет точечных объектов подключённых Data Products."}
</small>
) : null}
</div>
</section>
</div>
</WorkspaceWindow>
) : null}
{toolbarOpen ? (
<div className="catalog-map-fixture__toolbar" aria-label="Map toolbar" data-search-open={searchOpen || undefined}>
<Dropdown
@@ -2256,7 +2589,7 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
onClose={() => closeSubjectWindow(summary.bindingId)}
title={summary.displayName}
subtitle={`${summary.total} всего · ${visibleCount} на карте`}
active={activeSubjectBindingId === summary.bindingId}
active={activeWorkspaceWindowId === `binding:${summary.bindingId}`}
zIndex={state.window.zIndex}
minWidth={240}
minHeight={220}
@@ -2343,20 +2676,14 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
onRectChange={setSubjectCardRect}
maximized={subjectCardMaximized}
onMaximizedChange={setSubjectCardMaximized}
onActivate={() => {
const nextZIndex = Math.max(subjectCardZIndex, layersWindowZIndex, ...Object.values(subjectStates).map((state) => state.window.zIndex)) + 1;
setSubjectCardZIndex(nextZIndex);
setSubjectCardActive(true);
setLayersWindowActive(false);
setActiveSubjectBindingId(undefined);
}}
onActivate={() => activateWorkspaceWindow("subject-card")}
onClose={() => {
setSubjectCardOpen(false);
setSubjectCardActive(false);
clearActiveWorkspaceWindow("subject-card");
}}
title={selectedSubjectCard.title}
subtitle={selectedSubjectCard.sourceId}
active={subjectCardActive}
active={activeWorkspaceWindowId === "subject-card"}
zIndex={subjectCardZIndex}
minWidth={320}
minHeight={320}
@@ -2414,25 +2741,35 @@ export const MapFixturePreview = forwardRef<MapFixturePreviewHandle, {
{assistantOpen ? <div className="catalog-map-fixture__assistant"><strong>NODE.DC Assistant</strong><span>Контекст выбранной сущности готов к передаче.</span></div> : null}
<button type="button" className="catalog-map-fixture__resize" aria-label="Изменить высоту карты" onPointerDown={startResize}><span /></button>
<Window
open={inspectorOpen && Boolean(features.inspector)}
title="Настройки карты"
subtitle="MAP / draggable inspector"
placement="end"
draggable
closeOnBackdrop={false}
lockBodyScroll={false}
trapFocus={false}
className="catalog-map-fixture__map-settings-window"
onClose={() => setInspectorOpen(false)}
>
<Inspector
sections={inspectorSections}
openSections={inspectorOpenSections}
singleOpen
onOpenSectionsChange={setInspectorOpenSections}
/>
</Window>
{inspectorOpen && Boolean(features.inspector) ? (
<WorkspaceWindow
boundsRef={workspaceRef}
rect={settingsWindowRect}
onRectChange={setSettingsWindowRect}
maximized={settingsWindowMaximized}
onMaximizedChange={setSettingsWindowMaximized}
onActivate={() => activateWorkspaceWindow("settings")}
onClose={() => {
setInspectorOpen(false);
clearActiveWorkspaceWindow("settings");
}}
title="Настройки карты"
subtitle="MAP / inspector"
active={activeWorkspaceWindowId === "settings"}
zIndex={settingsWindowZIndex}
minWidth={360}
minHeight={380}
className="catalog-map-fixture__map-settings-window catalog-map-fixture__map-glass-window"
aria-label="Настройки карты"
>
<Inspector
sections={inspectorSections}
openSections={inspectorOpenSections}
singleOpen
onOpenSectionsChange={setInspectorOpenSections}
/>
</WorkspaceWindow>
) : null}
</div>
);
});