/** * Schema tham chiếu cho commit snapshot. * * Đây là file doc tự chứa, không import runtime types. * Mục tiêu là mô tả đúng shape dữ liệu hiện tại của editor/commit/replay * mà không phụ thuộc trực tiếp vào source code runtime. * * Ghi chú: * - Payload tạo commit hiện là `{ snapshot_json, edit_summary }`. * - `CommitSnapshot` hiện tương đương `EditorSnapshot`. * - Nhiều field root để optional vì frontend còn phải đọc snapshot cũ / partial. * - Replay actions trong dữ liệu thật dùng `params: unknown[]` theo positional tuple. * - Snapshot replay cũ còn `replay_features` sẽ được FE migrate sang `target_geometry_ids` khi load. * - Trước khi gửi API, frontend còn normalize thêm một số field, ví dụ `geometries[].type`. */ // ---- Root request ---- export type CreateCommitRequest = { snapshot_json: CommitSnapshot; edit_summary: string; }; // ---- GeoJSON / FeatureCollection ---- export type GeometryPreset = "line" | "polygon" | "circle-area" | "point"; export type Geometry = | ({ type: "Point"; coordinates: [number, number] } & CircleGeometryMetadata) | ({ type: "MultiPoint"; coordinates: [number, number][] } & CircleGeometryMetadata) | ({ type: "LineString"; coordinates: [number, number][] } & CircleGeometryMetadata) | ({ type: "MultiLineString"; coordinates: [number, number][][] } & CircleGeometryMetadata) | ({ type: "Polygon"; coordinates: [number, number][][] } & CircleGeometryMetadata) | ({ type: "MultiPolygon"; coordinates: [number, number][][][] } & CircleGeometryMetadata); export type CircleGeometryMetadata = { circle_center?: [number, number]; circle_radius?: number; }; export type FeatureId = string | number; export type FeatureProperties = { id: FeatureId; type?: string | null; geometry_preset?: GeometryPreset | null; time_start?: number | null; time_end?: number | null; binding?: string[]; // UI/editor-only denormalized fields. entity_id?: string | null; entity_ids?: string[]; entity_name?: string | null; entity_names?: string[]; entity_type_id?: string | null; point_label?: string | null; line_label?: string | null; polygon_label?: string | null; }; export type Feature = { type: "Feature"; properties: FeatureProperties; geometry: Geometry; }; export type FeatureCollection = { type: "FeatureCollection"; features: Feature[]; }; // ---- Snapshot rows ---- export type SnapshotSource = "inline" | "ref"; export type SnapshotOperation = "create" | "update" | "delete" | "reference"; export type EntitySnapshotOperation = SnapshotOperation; export type GeometrySnapshotOperation = SnapshotOperation; export type WikiSnapshotOperation = SnapshotOperation; export type EntitySnapshot = { id: string; source: SnapshotSource; operation?: EntitySnapshotOperation; name?: string; description?: string | null; }; export type GeometrySnapshot = { id: string; source: SnapshotSource; operation?: GeometrySnapshotOperation; type?: string | null; draw_geometry?: Geometry; geometry?: Geometry; binding?: string[]; time_start?: number | null; time_end?: number | null; bbox?: { min_lng: number; min_lat: number; max_lng: number; max_lat: number; } | null; }; export type GeometryEntitySnapshot = { geometry_id: string; entity_id: string; operation?: "reference" | "binding" | "delete"; }; export type WikiDoc = string | null; export type WikiSnapshot = { id: string; source: SnapshotSource; operation?: WikiSnapshotOperation; title: string; slug?: string | null; doc: WikiDoc; }; export type EntityWikiLinkSnapshot = { entity_id: string; wiki_id: string; operation?: "reference" | "binding" | "delete"; }; // ---- Replay / Scripting System (runtime shape) ---- /** * Canonical UI action names trong snapshot hiện tại. * Không còn wrapper `function_name: "UI"` trong shape mới. */ export type UIOptionName = | "timeline" | "layer_panel" | "wiki_panel" | "zoom_panel" | "wiki" | "toast" | "wiki_header" | "playback_speed"; export type MapFunctionName = | "set_camera_view" | "set_time_filter" | "enable_timeline_filter" | "disable_timeline_filter" | "toggle_labels" | "show_labels" | "hide_labels" | "reset_camera_north"; export type GeoFunctionName = | "fly_to_geometry" | "fly_to_geometries" | "set_geometry_visibility" | "show_geometries" | "hide_geometries" | "fit_to_geometries" | "orbit_camera_around_geometry" | "pulse_geometry" | "animate_dashed_border" | "set_geometry_style" | "show_geometry_label" | "follow_geometry_path" | "follow_geometries_path" | "dim_other_geometries"; export type NarrativeFunctionName = | "set_title" | "set_descriptions" | "show_dialog_box" | "display_historical_image" | "set_step_subtitle"; /** * Runtime thật hiện dùng positional array cho params. * File doc này giữ đúng shape đó. */ export type ReplayAction = { function_name: T; params: unknown[]; }; export type ReplayStep = { duration: number; use_UI_function: ReplayAction[]; use_map_function: ReplayAction[]; use_geo_function: ReplayAction[]; use_narrow_function: ReplayAction[]; }; export type ReplayStage = { id: number; title?: string; detail_time_start: string; detail_time_stop: string; steps: ReplayStep[]; }; export type BattleReplay = { geometry_id: string; target_geometry_ids: string[]; detail: ReplayStage[]; }; // ---- Replay tuple docs ---- /** * Doc-only helper để giải thích meaning của từng vị trí trong `params`. * Runtime không ép các tuple này; chúng chỉ là tài liệu tham chiếu. */ export type ReplayCameraViewStateDoc = { center?: [number, number] | { lng: number; lat: number }; zoom?: number; pitch?: number; bearing?: number; duration?: number; }; export type ReplayUiParamTupleDocs = { timeline: [visible: boolean]; layer_panel: [visible: boolean]; wiki_panel: [visible: boolean]; zoom_panel: [visible: boolean]; wiki: [wiki_id: string]; toast: [message: string]; wiki_header: [header_id: string]; playback_speed: [speed: number]; }; /** * Snapshot cũ kiểu `function_name: "UI"` chỉ còn là legacy input. * Frontend hiện normalize chúng sang `function_name: UIOptionName` khi load. */ export type ReplayMapFunctionParamTupleDocs = { set_camera_view: [state: ReplayCameraViewStateDoc]; set_time_filter: [year: number]; enable_timeline_filter: []; disable_timeline_filter: []; toggle_labels: [visible: boolean]; show_labels: []; hide_labels: []; reset_camera_north: []; }; export type ReplayGeoFunctionParamTupleDocs = { fly_to_geometry: [ geometry_id: string, zoom?: number, padding?: number, duration?: number, ]; fly_to_geometries: [geometry_ids: string[]]; set_geometry_visibility: [geometry_ids: string[], visible: boolean]; show_geometries: [geometry_ids: string[]]; hide_geometries: [geometry_ids: string[]]; fit_to_geometries: [ geometry_ids: string[], padding?: number, duration?: number, ]; orbit_camera_around_geometry: [ geometry_id: string, zoom?: number, pitch?: number, revolutions?: number, duration?: number, ]; pulse_geometry: [ geometry_id: string, color?: string, repeat?: number, duration?: number, ]; animate_dashed_border: [ geometry_id: string, color?: string, width?: number, speed?: number, duration?: number, ]; set_geometry_style: [ geometry_ids: string[], fill_color?: string, fill_opacity?: number, line_color?: string, line_width?: number, ]; show_geometry_label: [ geometry_id: string, text?: string, color?: string, size?: number, ]; follow_geometry_path: [ geometry_id: string, duration?: number, zoom?: number, pitch?: number, ]; follow_geometries_path: [ geometry_ids: string[], duration?: number, zoom?: number, pitch?: number, ]; dim_other_geometries: [ geometry_ids: string[], opacity?: number, ]; }; export type ReplayNarrativeParamTupleDocs = { set_title: [title: string]; set_descriptions: [text: string]; show_dialog_box: [ avatar: string, text: string, side?: "left" | "right", speaker?: string, ]; display_historical_image: [ url: string, caption?: string, ]; set_step_subtitle: [subtitle: string | null]; }; export type ReplayParamTupleDocs = & ReplayUiParamTupleDocs & ReplayMapFunctionParamTupleDocs & ReplayGeoFunctionParamTupleDocs & ReplayNarrativeParamTupleDocs; export type ReplayActionTupleDoc = { function_name: T; params: ReplayParamTupleDocs[T]; }; // ---- Snapshot root ---- export type EditorSnapshot = { // Legacy snapshots có thể còn field project embedded. project?: { id: string; title: string; }; editor_feature_collection?: FeatureCollection; entities?: EntitySnapshot[]; geometries?: GeometrySnapshot[]; geometry_entity?: GeometryEntitySnapshot[]; wikis?: WikiSnapshot[]; entity_wiki?: EntityWikiLinkSnapshot[]; replays?: BattleReplay[]; }; export type CommitSnapshot = EditorSnapshot;