FEAT - FOUNDRY: activate sector workspace
This commit is contained in:
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user