finish refactor pre merge
This commit is contained in:
@@ -3,7 +3,9 @@ import type { FeatureCollection } from "@/types/geo";
|
||||
import { deepClone } from "@/lib/editor/draft/draftDiff";
|
||||
|
||||
export function useDraftState(initialData: FeatureCollection) {
|
||||
// Draft hiện tại (React state) để UI re-render khi dữ liệu thay đổi.
|
||||
const [draft, setDraft] = useState<FeatureCollection>(() => deepClone(initialData));
|
||||
// Draft ref để đọc giá trị mới nhất trong event handlers/engines mà không cần deps.
|
||||
const draftRef = useRef<FeatureCollection>(deepClone(initialData));
|
||||
|
||||
const commitDraft = useCallback((nextDraft: FeatureCollection) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ type Options = {
|
||||
|
||||
export function useUndoStack(options: Options) {
|
||||
const { applyUndoAction } = options;
|
||||
// Stack thao tác undo (append-only, pop khi undo).
|
||||
const [undoStack, setUndoStack] = useState<UndoAction[]>([]);
|
||||
|
||||
const pushUndo = useCallback((action: UndoAction) => {
|
||||
|
||||
@@ -5,9 +5,11 @@ import {
|
||||
} from "@/lib/backgroundLayers";
|
||||
|
||||
export function useBackgroundSessionState() {
|
||||
// Trạng thái bật/tắt layer nền (khởi tạo default hidden; sẽ load từ storage ở page).
|
||||
const [backgroundVisibility, setBackgroundVisibility] = useState<BackgroundLayerVisibility>(
|
||||
() => ({ ...HIDDEN_BACKGROUND_LAYER_VISIBILITY })
|
||||
);
|
||||
// Đảm bảo đã load visibility trước khi render map thật.
|
||||
const [isBackgroundVisibilityReady, setIsBackgroundVisibilityReady] = useState(false);
|
||||
|
||||
return {
|
||||
@@ -17,4 +19,3 @@ export function useBackgroundSessionState() {
|
||||
setIsBackgroundVisibilityReady,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -10,27 +10,41 @@ import type {
|
||||
} from "@/lib/editor/session/sessionTypes";
|
||||
|
||||
export function useEntitySessionState() {
|
||||
// Entities đã persisted từ backend (dùng cho search/binding).
|
||||
const [persistedEntities, setPersistedEntities] = useState<Entity[]>([]);
|
||||
// Entities tạo mới trong phiên nhưng chưa commit lên backend.
|
||||
const [pendingEntityCreates, setPendingEntityCreates] = useState<PendingEntityCreate[]>([]);
|
||||
// Tóm tắt entities đã tạo (để hiển thị nhanh ở sidebar).
|
||||
const [createdEntities, setCreatedEntities] = useState<CreatedEntitySummary[]>([]);
|
||||
// Thông báo trạng thái/lỗi liên quan entity/session.
|
||||
const [entityStatus, setEntityStatus] = useState<string | null>(null);
|
||||
// Feature đang được chọn để thao tác bind entities/metadata.
|
||||
const [selectedFeatureId, setSelectedFeatureId] = useState<FeatureId | null>(null);
|
||||
// Form tạo entity mới (độc lập).
|
||||
const [entityForm, setEntityForm] = useState<EntityFormState>({
|
||||
name: "",
|
||||
slug: "",
|
||||
type_id: DEFAULT_ENTITY_TYPE_ID,
|
||||
});
|
||||
// Danh sách entity IDs đang chọn để bind vào geometry hiện tại.
|
||||
const [selectedGeometryEntityIds, setSelectedGeometryEntityIds] = useState<string[]>([]);
|
||||
// Form metadata geometry (time range + binding ids).
|
||||
const [geometryMetaForm, setGeometryMetaForm] = useState<GeometryMetaFormState>({
|
||||
time_start: "",
|
||||
time_end: "",
|
||||
binding: "",
|
||||
});
|
||||
// Cờ loading khi apply entity/metadata (local submit).
|
||||
const [isEntitySubmitting, setIsEntitySubmitting] = useState(false);
|
||||
// Thông báo trạng thái/lỗi cho form entity/metadata.
|
||||
const [entityFormStatus, setEntityFormStatus] = useState<string | null>(null);
|
||||
// Keyword search entity theo name.
|
||||
const [entitySearchQuery, setEntitySearchQuery] = useState("");
|
||||
// Kết quả search entity để user chọn.
|
||||
const [entitySearchResults, setEntitySearchResults] = useState<Entity[]>([]);
|
||||
// Entity ID đang được chọn trong dropdown kết quả search.
|
||||
const [selectedSearchEntityId, setSelectedSearchEntityId] = useState<string | null>(null);
|
||||
// Cờ loading khi search entity.
|
||||
const [isEntitySearchLoading, setIsEntitySearchLoading] = useState(false);
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,6 +9,7 @@ type Options = {
|
||||
type SectionTask = "idle" | "saving" | "submitting" | "opening-section";
|
||||
|
||||
export function useSectionSessionState(options: Options) {
|
||||
// Single state machine cho các tác vụ async của section (saving/submitting/opening).
|
||||
const [sectionTask, setSectionTask] = useState<SectionTask>("idle");
|
||||
const setTaskFlag = useCallback((task: Exclude<SectionTask, "idle">, next: SetStateAction<boolean>) => {
|
||||
setSectionTask((prev) => {
|
||||
@@ -32,15 +33,25 @@ export function useSectionSessionState(options: Options) {
|
||||
setTaskFlag("opening-section", next);
|
||||
}, [setTaskFlag]);
|
||||
|
||||
// Danh sách sections để user chọn mở.
|
||||
const [availableSections, setAvailableSections] = useState<Section[]>([]);
|
||||
// Section ID đang được chọn trong dropdown.
|
||||
const [selectedSectionId, setSelectedSectionId] = useState("");
|
||||
// Title section mới (để create).
|
||||
const [newSectionTitle, setNewSectionTitle] = useState("");
|
||||
// Input title cho commit.
|
||||
const [commitTitle, setCommitTitle] = useState("");
|
||||
// Input note cho commit.
|
||||
const [commitNote, setCommitNote] = useState("");
|
||||
// User ID dùng để gắn vào commit/submit/lock.
|
||||
const [editorUserIdInput, setEditorUserIdInput] = useState(options.defaultEditorUserId);
|
||||
// Section đang mở để edit (null nếu chưa mở).
|
||||
const [activeSection, setActiveSection] = useState<Section | null>(null);
|
||||
// Trạng thái section (version/head/status/lock).
|
||||
const [sectionState, setSectionState] = useState<SectionState | null>(null);
|
||||
// Danh sách commits của section đang mở.
|
||||
const [sectionCommits, setSectionCommits] = useState<SectionCommit[]>([]);
|
||||
// Snapshot gần nhất đã load (để build snapshot diff/metadata).
|
||||
const [lastSectionSnapshot, setLastSectionSnapshot] = useState<EditorSnapshot | null>(null);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import type { TimelineRange } from "@/lib/editor/session/sessionTypes";
|
||||
import { clampYearValue } from "@/lib/timeline";
|
||||
|
||||
type Options = {
|
||||
currentYear: number;
|
||||
@@ -7,6 +8,7 @@ type Options = {
|
||||
};
|
||||
|
||||
export function useTimelineState(options: Options) {
|
||||
// Năm timeline "đã chốt" để fetch dữ liệu.
|
||||
const [timelineYear, setTimelineYear] = useState<number>(() =>
|
||||
clampYearValue(
|
||||
options.currentYear,
|
||||
@@ -14,6 +16,7 @@ export function useTimelineState(options: Options) {
|
||||
options.fallbackTimelineRange.max
|
||||
)
|
||||
);
|
||||
// Năm timeline đang chỉnh (debounce rồi đẩy sang timelineYear).
|
||||
const [timelineDraftYear, setTimelineDraftYear] = useState<number>(() =>
|
||||
clampYearValue(
|
||||
options.currentYear,
|
||||
@@ -21,7 +24,9 @@ export function useTimelineState(options: Options) {
|
||||
options.fallbackTimelineRange.max
|
||||
)
|
||||
);
|
||||
// Cờ loading khi fetch theo timeline.
|
||||
const [isTimelineLoading, setIsTimelineLoading] = useState(false);
|
||||
// Thông báo trạng thái/lỗi khi fetch theo timeline.
|
||||
const [timelineStatus, setTimelineStatus] = useState<string | null>(null);
|
||||
|
||||
return {
|
||||
@@ -35,11 +40,3 @@ export function useTimelineState(options: Options) {
|
||||
setTimelineStatus,
|
||||
};
|
||||
}
|
||||
|
||||
function clampYearValue(year: number, minYear: number, maxYear: number): number {
|
||||
const lower = Math.min(minYear, maxYear);
|
||||
const upper = Math.max(minYear, maxYear);
|
||||
if (year < lower) return lower;
|
||||
if (year > upper) return upper;
|
||||
return year;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { Geometry } from "@/lib/useEditorState";
|
||||
|
||||
type ModeGetter = () => "idle" | "draw" | "select" | "add-point" | "add-line" | "add-path" | "add-circle";
|
||||
import type { ModeGetter } from "@/lib/engine/engineTypes";
|
||||
|
||||
const EARTH_RADIUS_METERS = 6371008.8;
|
||||
const CIRCLE_SEGMENTS = 72;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { Geometry } from "@/lib/useEditorState";
|
||||
|
||||
type ModeGetter = () => "idle" | "draw" | "select" | "add-point" | "add-line" | "add-path" | "add-circle";
|
||||
import type { ModeGetter } from "@/lib/engine/engineTypes";
|
||||
|
||||
// Khởi tạo engine vẽ polygon tự do theo chuỗi click.
|
||||
export function initDrawing(
|
||||
|
||||
4
lib/engine/engineTypes.ts
Normal file
4
lib/engine/engineTypes.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import type { EditorMode } from "@/lib/editor/session/sessionTypes";
|
||||
|
||||
export type ModeGetter = () => EditorMode;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { Geometry } from "@/lib/useEditorState";
|
||||
|
||||
type ModeGetter = () => "idle" | "draw" | "select" | "add-point" | "add-line" | "add-path" | "add-circle";
|
||||
import type { ModeGetter } from "@/lib/engine/engineTypes";
|
||||
|
||||
const EMPTY_PREVIEW: GeoJSON.FeatureCollection = {
|
||||
type: "FeatureCollection",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { Geometry } from "@/lib/useEditorState";
|
||||
|
||||
type ModeGetter = () => "idle" | "draw" | "select" | "add-point" | "add-line" | "add-path" | "add-circle";
|
||||
import type { ModeGetter } from "@/lib/engine/engineTypes";
|
||||
|
||||
const EMPTY_PREVIEW: GeoJSON.FeatureCollection = {
|
||||
type: "FeatureCollection",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { Geometry } from "@/lib/useEditorState";
|
||||
|
||||
type ModeGetter = () => "idle" | "draw" | "select" | "add-point" | "add-line" | "add-path" | "add-circle";
|
||||
import type { ModeGetter } from "@/lib/engine/engineTypes";
|
||||
|
||||
// Khởi tạo engine thêm point bằng click đơn.
|
||||
export function initPoint(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
|
||||
type ModeGetter = () => "idle" | "draw" | "select" | "add-point" | "add-line" | "add-path" | "add-circle";
|
||||
import type { ModeGetter } from "@/lib/engine/engineTypes";
|
||||
|
||||
// Khởi tạo engine chọn feature và context menu edit/delete.
|
||||
export function initSelect(
|
||||
|
||||
14
lib/geo/constants.ts
Normal file
14
lib/geo/constants.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { FeatureCollection } from "@/types/geo";
|
||||
|
||||
export const WORLD_BBOX = {
|
||||
minLng: -180,
|
||||
minLat: -90,
|
||||
maxLng: 180,
|
||||
maxLat: 90,
|
||||
} as const;
|
||||
|
||||
export const EMPTY_FEATURE_COLLECTION: FeatureCollection = {
|
||||
type: "FeatureCollection",
|
||||
features: [],
|
||||
};
|
||||
|
||||
13
lib/map/constants.ts
Normal file
13
lib/map/constants.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export const DEFAULT_POINT_ICON_ID = "point-icon-default";
|
||||
export const POINT_ICON_URL = "/point.png";
|
||||
export const PATH_ARROW_ICON_ID = "path-arrow-icon";
|
||||
|
||||
export const MAP_MIN_ZOOM = 1;
|
||||
export const MAP_MAX_ZOOM = 10;
|
||||
|
||||
export const RASTER_BASE_SOURCE_ID = "rasterBase";
|
||||
export const RASTER_BASE_LAYER_ID = "raster-base-layer";
|
||||
export const RASTER_BASE_INSERT_BEFORE_LAYER_ID = "graticules-line";
|
||||
|
||||
export const PATH_ARROW_SOURCE_ID = "path-arrow-shapes";
|
||||
export const FEATURE_STATE_SOURCE_IDS = ["countries", "places", PATH_ARROW_SOURCE_ID] as const;
|
||||
76
lib/map/style.ts
Normal file
76
lib/map/style.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import maplibregl from "maplibre-gl";
|
||||
|
||||
export const COUNTRY_COLOR_KEY_EXPRESSION: maplibregl.ExpressionSpecification = [
|
||||
"coalesce",
|
||||
["get", "MAPCOLOR7"],
|
||||
["get", "MAPCOLOR9"],
|
||||
["get", "scalerank"],
|
||||
0,
|
||||
];
|
||||
|
||||
export const COUNTRY_FILL_COLOR_EXPRESSION: maplibregl.ExpressionSpecification = [
|
||||
"match",
|
||||
COUNTRY_COLOR_KEY_EXPRESSION,
|
||||
1, "#ef4444",
|
||||
2, "#f97316",
|
||||
3, "#f59e0b",
|
||||
4, "#22c55e",
|
||||
5, "#06b6d4",
|
||||
6, "#3b82f6",
|
||||
7, "#8b5cf6",
|
||||
8, "#a855f7",
|
||||
9, "#d946ef",
|
||||
10, "#14b8a6",
|
||||
"#64748b",
|
||||
];
|
||||
|
||||
export const POLYGON_FILL_BY_TYPE: Record<string, string> = {
|
||||
country: "#2563eb",
|
||||
state: "#0ea5e9",
|
||||
empire: "#f59e0b",
|
||||
kingdom: "#d97706",
|
||||
war: "#dc2626",
|
||||
battle: "#f43f5e",
|
||||
civilization: "#14b8a6",
|
||||
rebellion_zone: "#7c3aed",
|
||||
};
|
||||
|
||||
export const POLYGON_STROKE_BY_TYPE: Record<string, string> = {
|
||||
country: "#1e3a8a",
|
||||
state: "#0c4a6e",
|
||||
empire: "#7c2d12",
|
||||
kingdom: "#9a3412",
|
||||
war: "#7f1d1d",
|
||||
battle: "#9f1239",
|
||||
civilization: "#134e4a",
|
||||
rebellion_zone: "#4c1d95",
|
||||
};
|
||||
|
||||
export const POLYGON_OPACITY_BY_TYPE: Record<string, number> = {
|
||||
war: 0.3,
|
||||
battle: 0.34,
|
||||
civilization: 0.38,
|
||||
rebellion_zone: 0.32,
|
||||
};
|
||||
|
||||
export const LINE_COLOR_BY_TYPE: Record<string, string> = {
|
||||
defense_line: "#f97316",
|
||||
attack_route: "#ef4444",
|
||||
retreat_route: "#94a3b8",
|
||||
invasion_route: "#b91c1c",
|
||||
migration_route: "#0ea5e9",
|
||||
refugee_route: "#06b6d4",
|
||||
trade_route: "#eab308",
|
||||
shipping_route: "#2563eb",
|
||||
};
|
||||
|
||||
export const PATH_RENDER_BY_TYPE: Record<string, boolean> = {
|
||||
attack_route: true,
|
||||
retreat_route: true,
|
||||
invasion_route: true,
|
||||
migration_route: true,
|
||||
refugee_route: true,
|
||||
trade_route: true,
|
||||
shipping_route: true,
|
||||
};
|
||||
|
||||
25
lib/timeline.ts
Normal file
25
lib/timeline.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { TimelineRange } from "@/lib/editor/session/sessionTypes";
|
||||
|
||||
// Single source of truth for the app-wide timeline range.
|
||||
export const FIXED_TIMELINE_START_YEAR = -2000;
|
||||
export const FIXED_TIMELINE_END_YEAR = 2000;
|
||||
|
||||
export const FIXED_TIMELINE_RANGE: TimelineRange = {
|
||||
min: FIXED_TIMELINE_START_YEAR,
|
||||
max: FIXED_TIMELINE_END_YEAR,
|
||||
};
|
||||
|
||||
// UI debounce when user drags timeline before triggering data fetch.
|
||||
export const TIMELINE_DEBOUNCE_MS = 180;
|
||||
|
||||
export function clampYearValue(year: number, minYear: number, maxYear: number): number {
|
||||
const lower = Math.min(minYear, maxYear);
|
||||
const upper = Math.max(minYear, maxYear);
|
||||
if (year < lower) return lower;
|
||||
if (year > upper) return upper;
|
||||
return year;
|
||||
}
|
||||
|
||||
export function clampYearToFixedRange(year: number): number {
|
||||
return clampYearValue(year, FIXED_TIMELINE_START_YEAR, FIXED_TIMELINE_END_YEAR);
|
||||
}
|
||||
@@ -23,7 +23,9 @@ type Options = {
|
||||
};
|
||||
|
||||
export function useEditorSessionState(options: Options) {
|
||||
// Mode thao tác map/editor hiện tại.
|
||||
const [mode, setMode] = useState<EditorMode>("idle");
|
||||
// FeatureCollection "gốc" của session hiện tại (global timeline hoặc section snapshot).
|
||||
const [initialData, setInitialData] = useState<FeatureCollection>(options.emptyFeatureCollection);
|
||||
|
||||
const section = useSectionSessionState({
|
||||
|
||||
@@ -20,9 +20,11 @@ export type { Change, UndoAction } from "@/lib/editor/draft/editorTypes";
|
||||
export function useEditorState(initialData: FeatureCollection) {
|
||||
const { draft, draftRef, commitDraft, resetDraft } = useDraftState(initialData);
|
||||
|
||||
// Map baseline (id -> feature) để diff draft hiện tại ra changes.
|
||||
const initialMapRef = useRef<Map<FeatureProperties["id"], Feature>>(
|
||||
buildInitialMap(initialData)
|
||||
);
|
||||
// Version counter để ép diff recalculation sau khi reset/clear baseline.
|
||||
const [baselineVersion, setBaselineVersion] = useState(0);
|
||||
|
||||
const applyUndoAction = useCallback((action: UndoAction): boolean => {
|
||||
|
||||
Reference in New Issue
Block a user