refactor: pre serve route /
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImperativeHandle } from "react";
|
||||
import type { RefObject, Dispatch, SetStateAction } from "react";
|
||||
import { type MapFeaturePayload, type MapHandle } from "@/uhm/components/Map";
|
||||
import type { MapHoverPopupContent } from "@/uhm/components/map/useMapHoverPopup";
|
||||
import PresentPlaceSearch, { type HistoricalGeometryFocusPayload, type PresentPlaceSelection } from "@/uhm/components/editor/PresentPlaceSearch";
|
||||
import ReplayPreviewOverlay from "@/uhm/components/editor/ReplayPreviewOverlay";
|
||||
import ReplayPreviewLayerPanel from "@/uhm/components/editor/ReplayPreviewLayerPanel";
|
||||
@@ -10,14 +12,15 @@ import TimelineBar from "@/uhm/components/ui/TimelineBar";
|
||||
import RelatedEntityPopup from "./RelatedEntityPopup";
|
||||
import PinnedWikiPopup from "./PinnedWikiPopup";
|
||||
|
||||
import { type Wiki } from "@/uhm/api/wikis";
|
||||
import { fetchWikiById, type Wiki } from "@/uhm/api/wikis";
|
||||
import type { Entity } from "@/uhm/api/entities";
|
||||
import type { FeatureCollection } from "@/uhm/types/geo";
|
||||
import type { BattleReplay } from "@/uhm/types/projects";
|
||||
import type { BattleReplay, EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||
import { type BackgroundLayerVisibility } from "@/uhm/lib/map/styles/backgroundLayers";
|
||||
import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants";
|
||||
import { normalizeFeatureEntityIds } from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
||||
import type { PreviewRelationIndex } from "@/uhm/lib/preview/types";
|
||||
import type { Feature } from "@/uhm/lib/editor/state/useEditorState";
|
||||
|
||||
type Props = {
|
||||
projectId: string;
|
||||
@@ -28,36 +31,43 @@ type Props = {
|
||||
replays: BattleReplay[];
|
||||
entities: Entity[];
|
||||
wikis: WikiSnapshot[];
|
||||
entityWikiLinks?: EntityWikiLinkSnapshot[];
|
||||
backgroundVisibility: BackgroundLayerVisibility;
|
||||
onBackgroundVisibilityChange: (vis: BackgroundLayerVisibility) => void;
|
||||
geometryVisibility: Record<string, boolean>;
|
||||
onGeometryVisibilityChange: (vis: Record<string, boolean>) => void;
|
||||
viewMode?: "local" | "global";
|
||||
onViewModeChange?: (mode: "local" | "global") => void;
|
||||
globalGeometries?: FeatureCollection;
|
||||
isGlobalLoading?: boolean;
|
||||
baseline?: FeatureCollection;
|
||||
activeReplay?: BattleReplay | null;
|
||||
selectedStageId?: number | null;
|
||||
selectedStepIndex?: number | null;
|
||||
autoplayMode?: "start" | "selection" | null;
|
||||
|
||||
replayPreview: any;
|
||||
mapHandleRef?: RefObject<MapHandle | null>;
|
||||
previewRelations: PreviewRelationIndex;
|
||||
previewActiveEntityId: string | null;
|
||||
setPreviewActiveEntityId: (id: string | null) => void;
|
||||
setPreviewEntityFocusToken: React.Dispatch<React.SetStateAction<number>>;
|
||||
previewEntityFocusToken?: number;
|
||||
setPreviewEntityFocusToken: Dispatch<SetStateAction<number>>;
|
||||
previewSidebarWidth: number;
|
||||
setPreviewSidebarWidth: React.Dispatch<React.SetStateAction<number>>;
|
||||
setPreviewSidebarWidth: Dispatch<SetStateAction<number>>;
|
||||
previewWikiCache: Record<string, Wiki>;
|
||||
setPreviewWikiCache: React.Dispatch<React.SetStateAction<Record<string, Wiki>>>;
|
||||
setPreviewWikiCache: Dispatch<SetStateAction<Record<string, Wiki>>>;
|
||||
isLargeScreen?: boolean;
|
||||
setIsLargeScreen?: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
type PreviewRelationIndex = {
|
||||
entitiesById: Record<string, Entity>;
|
||||
entityGeometriesById: Record<string, FeatureCollection>;
|
||||
entityWikisById: Record<string, Wiki[]>;
|
||||
geometryEntityIds: Record<string, string[]>;
|
||||
wikiEntityIdsById: Record<string, string[]>;
|
||||
wikiEntityIdsBySlug: Record<string, string[]>;
|
||||
wikiById: Record<string, Wiki>;
|
||||
wikiBySlug: Record<string, Wiki>;
|
||||
export type PreviewLayoutHandle = {
|
||||
handleFeatureClick: (payload: MapFeaturePayload | null) => void;
|
||||
getHoverPopupContent: (feature: Feature) => MapHoverPopupContent | null;
|
||||
handlePlaySelectedReplay: (replay: BattleReplay) => void;
|
||||
};
|
||||
|
||||
const PreviewLayout = forwardRef<any, Props>(({
|
||||
const PreviewLayout = forwardRef<PreviewLayoutHandle, Props>(({
|
||||
projectId,
|
||||
mode,
|
||||
onModeChange,
|
||||
@@ -636,6 +646,8 @@ const PreviewLayout = forwardRef<any, Props>(({
|
||||
isLoading={false}
|
||||
disabled={isReplayPreviewMode}
|
||||
statusText={null}
|
||||
filterEnabled={replayPreview.timelineFilterEnabled}
|
||||
onFilterEnabledChange={replayPreview.setTimelineFilterEnabled}
|
||||
style={
|
||||
isReplayPreviewWikiSidebarOpen
|
||||
? { right: `${previewSidebarWidth + 32}px` }
|
||||
@@ -704,3 +716,10 @@ function computeFixedPopupPosition(rect: DOMRect, width: number, height: number)
|
||||
|
||||
return { top, left };
|
||||
}
|
||||
|
||||
function clampNumber(value: number, min: number, max: number): number {
|
||||
if (!Number.isFinite(value)) return min;
|
||||
if (value < min) return min;
|
||||
if (value > max) return max;
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import type { Entity } from "@/uhm/api/entities";
|
||||
import type { Wiki } from "@/uhm/api/wikis";
|
||||
import { normalizeFeatureEntityIds } from "@/uhm/lib/editor/snapshot/editorSnapshot";
|
||||
import { normalizeTimelineYearValue } from "@/uhm/lib/utils/timeline";
|
||||
import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||
import type { FeatureCollection } from "@/uhm/types/geo";
|
||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||
import type { PreviewRelationIndex } from "./types";
|
||||
|
||||
export function buildSnapshotPreviewRelationIndex(options: {
|
||||
draft: FeatureCollection;
|
||||
entities: Entity[];
|
||||
wikis: WikiSnapshot[];
|
||||
entityWikiLinks: EntityWikiLinkSnapshot[];
|
||||
wikiCache: Record<string, Wiki>;
|
||||
projectId: string;
|
||||
}): PreviewRelationIndex {
|
||||
const next = createEmptyPreviewRelationIndex();
|
||||
|
||||
for (const entity of options.entities || []) {
|
||||
const id = String(entity?.id || "").trim();
|
||||
if (!id) continue;
|
||||
next.entitiesById[id] = entity;
|
||||
}
|
||||
|
||||
for (const wikiSnapshot of options.wikis || []) {
|
||||
if (!wikiSnapshot || wikiSnapshot.operation === "delete") continue;
|
||||
const wiki = snapshotWikiToWiki(wikiSnapshot, options.wikiCache, options.projectId);
|
||||
if (!wiki?.id) continue;
|
||||
next.wikiById[wiki.id] = wiki;
|
||||
const slug = String(wiki.slug || "").trim();
|
||||
if (slug) next.wikiBySlug[slug] = wiki;
|
||||
}
|
||||
|
||||
for (const feature of options.draft.features || []) {
|
||||
const geometryId = String(feature.properties.id);
|
||||
for (const entityId of normalizeFeatureEntityIds(feature)) {
|
||||
if (!next.entitiesById[entityId]) {
|
||||
next.entitiesById[entityId] = { id: entityId, name: entityId };
|
||||
}
|
||||
pushUniqueString(next.geometryEntityIds, geometryId, entityId);
|
||||
pushFeatureForEntity(next, entityId, feature);
|
||||
}
|
||||
}
|
||||
|
||||
for (const link of options.entityWikiLinks || []) {
|
||||
if (!link || link.operation === "delete") continue;
|
||||
const entityId = String(link.entity_id || "").trim();
|
||||
const wikiId = String(link.wiki_id || "").trim();
|
||||
const entity = next.entitiesById[entityId] || null;
|
||||
const wiki = next.wikiById[wikiId] || null;
|
||||
if (!entity || !wiki) continue;
|
||||
|
||||
pushWikiForEntity(next, entityId, wiki);
|
||||
}
|
||||
|
||||
normalizePreviewRelationArrays(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function buildPublicPreviewRelationIndex(options: {
|
||||
entities: Entity[];
|
||||
entityGeometriesById: Record<string, FeatureCollection>;
|
||||
entityWikisById: Record<string, Wiki[]>;
|
||||
}): PreviewRelationIndex {
|
||||
const next = createEmptyPreviewRelationIndex();
|
||||
|
||||
for (const entity of options.entities || []) {
|
||||
const id = String(entity?.id || "").trim();
|
||||
if (!id) continue;
|
||||
next.entitiesById[id] = entity;
|
||||
}
|
||||
|
||||
for (const [entityId, geometries] of Object.entries(options.entityGeometriesById || {})) {
|
||||
const id = String(entityId || "").trim();
|
||||
if (!id) continue;
|
||||
if (!next.entitiesById[id]) next.entitiesById[id] = { id, name: id };
|
||||
|
||||
for (const feature of geometries.features || []) {
|
||||
const geometryId = String(feature.properties.id);
|
||||
pushUniqueString(next.geometryEntityIds, geometryId, id);
|
||||
pushFeatureForEntity(next, id, feature);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [entityId, wikis] of Object.entries(options.entityWikisById || {})) {
|
||||
const id = String(entityId || "").trim();
|
||||
if (!id) continue;
|
||||
if (!next.entitiesById[id]) next.entitiesById[id] = { id, name: id };
|
||||
|
||||
for (const wiki of wikis || []) {
|
||||
if (!wiki?.id) continue;
|
||||
next.wikiById[wiki.id] = wiki;
|
||||
const slug = String(wiki.slug || "").trim();
|
||||
if (slug) next.wikiBySlug[slug] = wiki;
|
||||
pushWikiForEntity(next, id, wiki);
|
||||
}
|
||||
}
|
||||
|
||||
normalizePreviewRelationArrays(next);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function buildEntityLabelContextDraft(
|
||||
draft: FeatureCollection,
|
||||
relationsOrEntities: PreviewRelationIndex | Entity[]
|
||||
): FeatureCollection {
|
||||
if (!draft.features.length) return draft;
|
||||
|
||||
const resolveEntityIds = Array.isArray(relationsOrEntities)
|
||||
? (feature: FeatureCollection["features"][number]) => normalizeFeatureEntityIds(feature)
|
||||
: (feature: FeatureCollection["features"][number]) =>
|
||||
relationsOrEntities.geometryEntityIds[String(feature.properties.id)] || normalizeFeatureEntityIds(feature);
|
||||
|
||||
const entityById = new globalThis.Map<string, Entity>();
|
||||
if (Array.isArray(relationsOrEntities)) {
|
||||
for (const entity of relationsOrEntities || []) {
|
||||
const id = String(entity?.id || "").trim();
|
||||
if (!id) continue;
|
||||
entityById.set(id, entity);
|
||||
}
|
||||
} else {
|
||||
for (const [id, entity] of Object.entries(relationsOrEntities.entitiesById)) {
|
||||
if (!id || !entity) continue;
|
||||
entityById.set(id, entity);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...draft,
|
||||
features: draft.features.map((feature) => {
|
||||
const entityIds = resolveEntityIds(feature);
|
||||
if (!entityIds.length) return feature;
|
||||
|
||||
const candidates = entityIds.map((id) => {
|
||||
const entity = entityById.get(id) || null;
|
||||
const name = String(entity?.name || id).trim();
|
||||
if (!name) return null;
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
time_start: normalizeTimelineYearValue(entity?.time_start),
|
||||
time_end: normalizeTimelineYearValue(entity?.time_end),
|
||||
};
|
||||
}).filter((candidate) => candidate !== null);
|
||||
|
||||
return {
|
||||
...feature,
|
||||
properties: {
|
||||
...feature.properties,
|
||||
entity_id: entityIds[0] || null,
|
||||
entity_ids: entityIds,
|
||||
entity_name: candidates[0]?.name || null,
|
||||
entity_names: candidates.map((candidate) => candidate.name),
|
||||
entity_label_candidates: candidates,
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createEmptyPreviewRelationIndex(): PreviewRelationIndex {
|
||||
return {
|
||||
entitiesById: {},
|
||||
entityGeometriesById: {},
|
||||
entityWikisById: {},
|
||||
geometryEntityIds: {},
|
||||
wikiEntityIdsById: {},
|
||||
wikiEntityIdsBySlug: {},
|
||||
wikiById: {},
|
||||
wikiBySlug: {},
|
||||
};
|
||||
}
|
||||
|
||||
export function pushUniqueString(target: Record<string, string[]>, key: string, value: string) {
|
||||
if (!target[key]) {
|
||||
target[key] = [value];
|
||||
return;
|
||||
}
|
||||
if (!target[key].includes(value)) {
|
||||
target[key].push(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizePreviewRelationArrays(target: PreviewRelationIndex | Record<string, string[]>) {
|
||||
if (isPreviewRelationIndex(target)) {
|
||||
normalizeRecordArrays(target.geometryEntityIds);
|
||||
normalizeRecordArrays(target.wikiEntityIdsById);
|
||||
normalizeRecordArrays(target.wikiEntityIdsBySlug);
|
||||
return;
|
||||
}
|
||||
normalizeRecordArrays(target);
|
||||
}
|
||||
|
||||
function pushFeatureForEntity(
|
||||
target: PreviewRelationIndex,
|
||||
entityId: string,
|
||||
feature: FeatureCollection["features"][number]
|
||||
) {
|
||||
if (!target.entityGeometriesById[entityId]) {
|
||||
target.entityGeometriesById[entityId] = { type: "FeatureCollection", features: [] };
|
||||
}
|
||||
const geometryId = String(feature.properties.id);
|
||||
if (!target.entityGeometriesById[entityId].features.some((item) => String(item.properties.id) === geometryId)) {
|
||||
target.entityGeometriesById[entityId].features.push(feature);
|
||||
}
|
||||
}
|
||||
|
||||
function pushWikiForEntity(target: PreviewRelationIndex, entityId: string, wiki: Wiki) {
|
||||
if (!target.entityWikisById[entityId]) target.entityWikisById[entityId] = [];
|
||||
if (!target.entityWikisById[entityId].some((item) => item.id === wiki.id)) {
|
||||
target.entityWikisById[entityId].push(wiki);
|
||||
}
|
||||
|
||||
pushUniqueString(target.wikiEntityIdsById, wiki.id, entityId);
|
||||
const slug = String(wiki.slug || "").trim();
|
||||
if (slug) pushUniqueString(target.wikiEntityIdsBySlug, slug, entityId);
|
||||
}
|
||||
|
||||
function snapshotWikiToWiki(snapshot: WikiSnapshot, wikiCache: Record<string, Wiki>, projectId: string): Wiki {
|
||||
if (typeof snapshot.doc === "string") {
|
||||
return {
|
||||
id: snapshot.id,
|
||||
project_id: projectId,
|
||||
title: snapshot.title,
|
||||
slug: snapshot.slug ?? null,
|
||||
content: snapshot.doc || "",
|
||||
};
|
||||
}
|
||||
|
||||
return wikiCache[snapshot.id] || {
|
||||
id: snapshot.id,
|
||||
project_id: projectId,
|
||||
title: snapshot.title,
|
||||
slug: snapshot.slug ?? null,
|
||||
content: "",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRecordArrays(target: Record<string, string[]>) {
|
||||
for (const key of Object.keys(target)) {
|
||||
target[key] = Array.from(new Set(target[key]));
|
||||
}
|
||||
}
|
||||
|
||||
function isPreviewRelationIndex(value: PreviewRelationIndex | Record<string, string[]>): value is PreviewRelationIndex {
|
||||
return "geometryEntityIds" in value && "wikiEntityIdsById" in value;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Entity } from "@/uhm/api/entities";
|
||||
import type { Wiki } from "@/uhm/api/wikis";
|
||||
import type { FeatureCollection } from "@/uhm/types/geo";
|
||||
|
||||
export type PreviewDataScope = "project-snapshot" | "public-atlas";
|
||||
|
||||
export type PreviewRelationIndex = {
|
||||
entitiesById: Record<string, Entity>;
|
||||
entityGeometriesById: Record<string, FeatureCollection>;
|
||||
entityWikisById: Record<string, Wiki[]>;
|
||||
geometryEntityIds: Record<string, string[]>;
|
||||
wikiEntityIdsById: Record<string, string[]>;
|
||||
wikiEntityIdsBySlug: Record<string, string[]>;
|
||||
wikiById: Record<string, Wiki>;
|
||||
wikiBySlug: Record<string, Wiki>;
|
||||
};
|
||||
|
||||
export const EMPTY_PREVIEW_RELATIONS: PreviewRelationIndex = {
|
||||
entitiesById: {},
|
||||
entityGeometriesById: {},
|
||||
entityWikisById: {},
|
||||
geometryEntityIds: {},
|
||||
wikiEntityIdsById: {},
|
||||
wikiEntityIdsBySlug: {},
|
||||
wikiById: {},
|
||||
wikiBySlug: {},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user