init: replay mode

This commit is contained in:
taDuc
2026-05-14 03:41:58 +07:00
parent c92aaafc33
commit 8fc9456a6a
41 changed files with 619 additions and 396 deletions
+392 -330
View File
@@ -97,9 +97,12 @@ export default function Page() {
const localCreatedEntityIdsRef = useRef<Set<string>>(new Set()); const localCreatedEntityIdsRef = useRef<Set<string>>(new Set());
const lastSelectedFeatureIdRef = useRef<string | null>(null); const lastSelectedFeatureIdRef = useRef<string | null>(null);
const [replayFeatureId, setReplayFeatureId] = useState<string | number | null>(null);
const [hideOutside, setHideOutside] = useState(false);
const { const {
mode, mode,
setMode, setMode: internalSetMode,
initialData, initialData,
setInitialData, setInitialData,
isSaving, isSaving,
@@ -409,6 +412,52 @@ export default function Page() {
restoreCommit, restoreCommit,
} = sectionCommands; } = sectionCommands;
const setMode = useCallback((m: EditorMode, featureId?: string | number) => {
if (m === "replay" && featureId) {
setReplayFeatureId(featureId);
} else if (m !== "replay") {
setReplayFeatureId(null);
setHideOutside(false);
}
internalSetMode(m);
}, [internalSetMode]);
const onSetMode = setMode;
const effectiveGeometryVisibility = useMemo(() => {
const visibility: Record<string, boolean> = { ...geometryVisibility };
if (mode === "replay" && replayFeatureId) {
// Ẩn chính geo được chọn làm replay
visibility[String(replayFeatureId)] = false;
if (hideOutside) {
// Tìm feature đang replay để lấy danh sách binding
const replayFeature = editor.draft.features.find(
(f) => String(f.properties.id) === String(replayFeatureId)
);
const boundIds = new Set<string>();
if (replayFeature?.properties?.binding) {
replayFeature.properties.binding.forEach((id: string) => boundIds.add(String(id)));
}
// Ẩn tất cả các geo không nằm trong binding
editor.draft.features.forEach((f) => {
const fid = String(f.properties.id);
if (fid !== String(replayFeatureId) && !boundIds.has(fid)) {
visibility[fid] = false;
}
});
}
}
return visibility;
}, [geometryVisibility, mode, replayFeatureId, hideOutside, editor.draft.features]);
const onToggleHideOutside = useCallback(() => {
setHideOutside((prev) => !prev);
}, []);
const openProject = useCallback(async () => { const openProject = useCallback(async () => {
if (!projectId) return; if (!projectId) return;
try { try {
@@ -693,7 +742,7 @@ export default function Page() {
useEffect(() => { useEffect(() => {
if (!selectedFeatureIds || selectedFeatureIds.length === 0) return; if (!selectedFeatureIds || selectedFeatureIds.length === 0) return;
const stillExistIds = selectedFeatureIds.filter(id => const stillExistIds = selectedFeatureIds.filter(id =>
timelineVisibleDraft.features.some(feature => String(feature.properties.id) === String(id)) timelineVisibleDraft.features.some(feature => String(feature.properties.id) === String(id))
); );
if (stillExistIds.length !== selectedFeatureIds.length) { if (stillExistIds.length !== selectedFeatureIds.length) {
@@ -964,7 +1013,7 @@ export default function Page() {
bindingPatches, bindingPatches,
nextChecked ? "Bind geometry vào GEO" : "Unbind geometry khỏi GEO" nextChecked ? "Bind geometry vào GEO" : "Unbind geometry khỏi GEO"
); );
// Assume selectedFeature (the first one) reflects the representative binding in UI // Assume selectedFeature (the first one) reflects the representative binding in UI
const firstFeaturePrevBindings = normalizeFeatureBindingIds(selectedFeatures[0]); const firstFeaturePrevBindings = normalizeFeatureBindingIds(selectedFeatures[0]);
const firstFeatureHas = firstFeaturePrevBindings.includes(id); const firstFeatureHas = firstFeaturePrevBindings.includes(id);
@@ -1223,36 +1272,40 @@ export default function Page() {
return ( return (
<div style={{ display: "flex", minHeight: "100vh" }}> <div style={{ display: "flex", minHeight: "100vh" }}>
<Editor {mode !== "replay" && (
mode={mode} <>
setMode={setMode} <Editor
entityStatus={entityStatus} mode={mode}
onUndo={editor.undo} setMode={setMode}
onCommit={commitSection} entityStatus={entityStatus}
onSubmit={submitCurrentSection} onUndo={editor.undo}
onRestoreCommit={restoreCommit} onCommit={commitSection}
isSaving={isSaving} onSubmit={submitCurrentSection}
isSubmitting={isSubmitting} onRestoreCommit={restoreCommit}
sectionTitle={activeSection?.title || "Đang tải project"} isSaving={isSaving}
projectStatus={projectState?.status || "editing"} isSubmitting={isSubmitting}
commitTitle={commitTitle} sectionTitle={activeSection?.title || "Đang tải project"}
onCommitTitleChange={setCommitTitle} projectStatus={projectState?.status || "editing"}
commitCount={sectionCommits.length} commitTitle={commitTitle}
hasHeadCommit={Boolean(projectState?.head_commit_id)} onCommitTitleChange={setCommitTitle}
headCommitId={projectState?.head_commit_id || null} commitCount={sectionCommits.length}
latestCommitLabel={headCommit ? `Head: ${formatCommitTitle(headCommit)}` : null} hasHeadCommit={Boolean(projectState?.head_commit_id)}
commits={sectionCommits} headCommitId={projectState?.head_commit_id || null}
changesCount={pendingSaveCount} latestCommitLabel={headCommit ? `Head: ${formatCommitTitle(headCommit)}` : null}
undoStack={editor.undoStack} commits={sectionCommits}
width={leftPanelWidth} changesCount={pendingSaveCount}
/> undoStack={editor.undoStack}
width={leftPanelWidth}
/>
<ResizeHandle <ResizeHandle
title="Resize left panel" title="Resize left panel"
onDrag={(deltaX) => { onDrag={(deltaX) => {
setLeftPanelWidth((prev) => clampNumber(prev + deltaX, 220, 520)); setLeftPanelWidth((prev) => clampNumber(prev + deltaX, 220, 520));
}} }}
/> />
</>
)}
{blockedPendingSubmissionId ? ( {blockedPendingSubmissionId ? (
<div style={{ flex: 1, minHeight: "100vh", background: "#0b1220", color: "white", padding: "24px" }}> <div style={{ flex: 1, minHeight: "100vh", background: "#0b1220", color: "white", padding: "24px" }}>
@@ -1301,6 +1354,7 @@ export default function Page() {
{isBackgroundVisibilityReady ? ( {isBackgroundVisibilityReady ? (
<Map <Map
mode={mode} mode={mode}
onSetMode={setMode}
draft={timelineVisibleDraft} draft={timelineVisibleDraft}
labelContextDraft={editor.draft} labelContextDraft={editor.draft}
selectedFeatureIds={selectedFeatureIds} selectedFeatureIds={selectedFeatureIds}
@@ -1309,326 +1363,334 @@ export default function Page() {
onDeleteFeature={editor.deleteFeature} onDeleteFeature={editor.deleteFeature}
onUpdateFeature={editor.updateFeature} onUpdateFeature={editor.updateFeature}
backgroundVisibility={backgroundVisibility} backgroundVisibility={backgroundVisibility}
geometryVisibility={geometryVisibility} geometryVisibility={effectiveGeometryVisibility}
respectBindingFilter={geometryBindingFilterEnabled} respectBindingFilter={geometryBindingFilterEnabled}
focusFeatureCollection={geometryFocusRequest?.collection || null} focusFeatureCollection={geometryFocusRequest?.collection || null}
focusRequestKey={geometryFocusRequest?.key ?? null} focusRequestKey={geometryFocusRequest?.key ?? null}
focusPadding={96} focusPadding={96}
hideOutside={hideOutside}
onToggleHideOutside={onToggleHideOutside}
/> />
) : ( ) : (
<div style={{ width: "100%", height: "100%", background: "#0b1220" }} /> <div style={{ width: "100%", height: "100%", background: "#0b1220" }} />
)} )}
<TimelineBar {mode !== "replay" && (
year={timelineDraftYear} <TimelineBar
onYearChange={handleTimelineYearChange} year={timelineDraftYear}
isLoading={false} onYearChange={handleTimelineYearChange}
disabled={false} isLoading={false}
statusText={null} disabled={false}
filterEnabled={timelineFilterEnabled} statusText={null}
onFilterEnabledChange={setTimelineFilterEnabled} filterEnabled={timelineFilterEnabled}
/> onFilterEnabledChange={setTimelineFilterEnabled}
/>
)}
</div> </div>
) : blockedPendingSubmissionId ? null : ( ) : blockedPendingSubmissionId ? null : (
// Wiki-only mode: avoid mounting Map/Timeline (WebGL + geometry fetching) to reduce lag. // Wiki-only mode: avoid mounting Map/Timeline (WebGL + geometry fetching) to reduce lag.
<div style={{ flex: 1, minHeight: "100vh", background: "#0b1220" }} /> <div style={{ flex: 1, minHeight: "100vh", background: "#0b1220" }} />
)} )}
<ResizeHandle {mode !== "replay" && (
title="Resize right panel" <>
onDrag={(deltaX) => { <ResizeHandle
// dragging handle (between map and right panel): moving right increases right panel width title="Resize right panel"
setRightPanelWidth((prev) => clampNumber(prev - deltaX, 260, 720)); onDrag={(deltaX) => {
}} // dragging handle (between map and right panel): moving right increases right panel width
/> setRightPanelWidth((prev) => clampNumber(prev - deltaX, 260, 720));
}}
/>
<BackgroundLayersPanel <BackgroundLayersPanel
visibility={backgroundVisibility} visibility={backgroundVisibility}
onToggleLayer={handleToggleBackgroundLayer} onToggleLayer={handleToggleBackgroundLayer}
onShowAll={handleShowAllBackgroundLayers} onShowAll={handleShowAllBackgroundLayers}
onHideAll={handleHideAllBackgroundLayers} onHideAll={handleHideAllBackgroundLayers}
geometryVisibility={geometryVisibility} geometryVisibility={geometryVisibility}
onToggleGeometryType={(typeKey) => { onToggleGeometryType={(typeKey) => {
setGeometryVisibility((prev) => ({ ...prev, [typeKey]: prev[typeKey] === false })); setGeometryVisibility((prev) => ({ ...prev, [typeKey]: prev[typeKey] === false }));
}} }}
width={rightPanelWidth} width={rightPanelWidth}
topContent={ topContent={
<div style={{ display: "grid", gap: "12px" }}> <div style={{ display: "grid", gap: "12px" }}>
<UnifiedSearchBar <UnifiedSearchBar
kind={searchKind} kind={searchKind}
onKindChange={(next) => { onKindChange={(next) => {
setSearchKind(next); setSearchKind(next);
setSearchQuery(""); setSearchQuery("");
setSearchQueryDraft(""); setSearchQueryDraft("");
}} }}
query={searchQuery} query={searchQuery}
onQueryChange={setSearchQuery} onQueryChange={setSearchQuery}
onLocalQueryChange={setSearchQueryDraft} onLocalQueryChange={setSearchQueryDraft}
/> />
{searchKind === "entity" && searchQueryDraft.trim().length > 0 ? ( {searchKind === "entity" && searchQueryDraft.trim().length > 0 ? (
<div style={{ padding: 10, background: "#0b1220", borderRadius: 8, border: "1px solid #1f2937" }}> <div style={{ padding: 10, background: "#0b1220", borderRadius: 8, border: "1px solid #1f2937" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}> <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
<div style={{ fontWeight: 700, fontSize: 13, color: "white" }}>Entity Results</div> <div style={{ fontWeight: 700, fontSize: 13, color: "white" }}>Entity Results</div>
<div style={{ fontSize: 12, color: "#94a3b8" }}> <div style={{ fontSize: 12, color: "#94a3b8" }}>
{isEntitySearchLoading ? "Searching…" : `${entitySearchResults.length} results`} {isEntitySearchLoading ? "Searching…" : `${entitySearchResults.length} results`}
</div>
</div>
<div style={{ marginTop: 8, display: "grid", gap: 6 }}>
{entitySearchResults.slice(0, 8).map((e) => (
<div
key={e.id}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: 8,
borderRadius: 6,
border: "1px solid #1f2937",
background: "transparent",
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: "#e5e7eb", fontSize: 12, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{e.name}
</div>
<div style={{ color: "#94a3b8", fontSize: 11, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{e.id}
</div>
</div> </div>
<button
type="button"
onClick={() => handleAddEntityRefToProject(e)}
style={{
border: "none",
background: "#111827",
color: "#93c5fd",
cursor: "pointer",
borderRadius: 6,
padding: "6px 8px",
fontSize: 12,
fontWeight: 700,
}}
title="Add entity ref to project snapshot"
>
Add
</button>
</div> </div>
))} <div style={{ marginTop: 8, display: "grid", gap: 6 }}>
{!isEntitySearchLoading && entitySearchResults.length === 0 ? ( {entitySearchResults.slice(0, 8).map((e) => (
<div style={{ fontSize: 12, color: "#94a3b8" }}>No results.</div> <div
) : null} key={e.id}
</div> style={{
</div> display: "flex",
) : null} alignItems: "center",
gap: 8,
{searchKind === "wiki" && searchQueryDraft.trim().length > 0 ? ( padding: 8,
<div style={{ padding: 10, background: "#0b1220", borderRadius: 8, border: "1px solid #1f2937" }}> borderRadius: 6,
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}> border: "1px solid #1f2937",
<div style={{ fontWeight: 700, fontSize: 13, color: "white" }}>Wiki Results</div> background: "transparent",
<div style={{ fontSize: 12, color: "#94a3b8" }}> }}
{isWikiSearching ? "Searching…" : `${wikiSearchResults.length} results`} >
</div> <div style={{ flex: 1, minWidth: 0 }}>
</div> <div style={{ color: "#e5e7eb", fontSize: 12, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
<div style={{ marginTop: 8, display: "grid", gap: 6 }}> {e.name}
{wikiSearchResults.slice(0, 8).map((w) => (
<div
key={w.id}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: 8,
borderRadius: 6,
border: "1px solid #1f2937",
background: "transparent",
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: "#e5e7eb", fontSize: 12, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{(w.title || "").trim() || "Untitled wiki"}
</div>
<div style={{ color: "#94a3b8", fontSize: 11, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{w.id}
</div>
</div>
<button
type="button"
onClick={() => handleAddWikiRefToProject(w)}
style={{
border: "none",
background: "#111827",
color: "#93c5fd",
cursor: "pointer",
borderRadius: 6,
padding: "6px 8px",
fontSize: 12,
fontWeight: 700,
}}
title="Add wiki ref to project snapshot"
>
Add
</button>
</div>
))}
{!isWikiSearching && wikiSearchResults.length === 0 ? (
<div style={{ fontSize: 12, color: "#94a3b8" }}>No results.</div>
) : null}
</div>
</div>
) : null}
{searchKind === "geo" && searchQueryDraft.trim().length > 0 ? (
<div style={{ padding: 10, background: "#0b1220", borderRadius: 8, border: "1px solid #1f2937" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
<div style={{ fontWeight: 700, fontSize: 13, color: "white" }}>Geo Results</div>
<div style={{ fontSize: 12, color: "#94a3b8" }}>
{isGeoSearching ? "Searching…" : `${geoSearchResults.length} entities`}
</div>
</div>
<div style={{ marginTop: 8, display: "grid", gap: 8 }}>
{geoSearchResults.slice(0, 6).map((item) => (
<div
key={item.entity_id}
style={{
padding: 8,
borderRadius: 6,
border: "1px solid #1f2937",
background: "transparent",
display: "grid",
gap: 6,
}}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
<div style={{ minWidth: 0 }}>
<div style={{ color: "#e5e7eb", fontSize: 12, fontWeight: 700, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{item.name?.trim() || item.entity_id}
</div>
<div style={{ color: "#94a3b8", fontSize: 11, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{item.entity_id}
</div>
</div>
<div style={{ fontSize: 12, color: "#94a3b8", flex: "0 0 auto" }}>
{Array.isArray(item.geometries) ? item.geometries.length : 0} geos
</div>
</div>
{item.description?.trim() ? (
<div style={{ color: "#cbd5e1", fontSize: 12, lineHeight: 1.35 }}>
{item.description.trim()}
</div>
) : null}
{Array.isArray(item.geometries) && item.geometries.length ? (
<div style={{ display: "grid", gap: 6, maxHeight: 200, overflowY: "auto", paddingRight: 4 }}>
{item.geometries.map((geo) => (
<div
key={geo.id}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
padding: 8,
borderRadius: 6,
border: "1px solid #243244",
background: "#0f172a",
}}
>
<div style={{ minWidth: 0 }}>
<div style={{ color: "#e5e7eb", fontSize: 12, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
#{geo.id}
</div>
<div style={{ color: "#94a3b8", fontSize: 11 }}>
type: {geo.type || "unknown"}{" "}
{geo.time_start != null || geo.time_end != null
? `| time: ${geo.time_start ?? "?"}${geo.time_end ?? "?"}`
: ""}
</div>
</div>
<button
type="button"
onClick={() => handleImportGeoFromSearch(item, geo)}
style={{
border: "none",
background: "#111827",
color: "#93c5fd",
cursor: "pointer",
borderRadius: 6,
padding: "6px 8px",
fontSize: 12,
fontWeight: 700,
flex: "0 0 auto",
}}
title="Import geometry into current editor draft"
>
Import
</button>
</div> </div>
))} <div style={{ color: "#94a3b8", fontSize: 11, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{e.id}
</div>
</div>
<button
type="button"
onClick={() => handleAddEntityRefToProject(e)}
style={{
border: "none",
background: "#111827",
color: "#93c5fd",
cursor: "pointer",
borderRadius: 6,
padding: "6px 8px",
fontSize: 12,
fontWeight: 700,
}}
title="Add entity ref to project snapshot"
>
Add
</button>
</div> </div>
) : ( ))}
<div style={{ fontSize: 12, color: "#94a3b8" }}> {!isEntitySearchLoading && entitySearchResults.length === 0 ? (
No geometry linked. <div style={{ fontSize: 12, color: "#94a3b8" }}>No results.</div>
</div> ) : null}
)}
</div> </div>
))} </div>
{!isGeoSearching && geoSearchResults.length === 0 ? ( ) : null}
<div style={{ fontSize: 12, color: "#94a3b8" }}>No results.</div>
) : null} {searchKind === "wiki" && searchQueryDraft.trim().length > 0 ? (
</div> <div style={{ padding: 10, background: "#0b1220", borderRadius: 8, border: "1px solid #1f2937" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
<div style={{ fontWeight: 700, fontSize: 13, color: "white" }}>Wiki Results</div>
<div style={{ fontSize: 12, color: "#94a3b8" }}>
{isWikiSearching ? "Searching…" : `${wikiSearchResults.length} results`}
</div>
</div>
<div style={{ marginTop: 8, display: "grid", gap: 6 }}>
{wikiSearchResults.slice(0, 8).map((w) => (
<div
key={w.id}
style={{
display: "flex",
alignItems: "center",
gap: 8,
padding: 8,
borderRadius: 6,
border: "1px solid #1f2937",
background: "transparent",
}}
>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ color: "#e5e7eb", fontSize: 12, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{(w.title || "").trim() || "Untitled wiki"}
</div>
<div style={{ color: "#94a3b8", fontSize: 11, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{w.id}
</div>
</div>
<button
type="button"
onClick={() => handleAddWikiRefToProject(w)}
style={{
border: "none",
background: "#111827",
color: "#93c5fd",
cursor: "pointer",
borderRadius: 6,
padding: "6px 8px",
fontSize: 12,
fontWeight: 700,
}}
title="Add wiki ref to project snapshot"
>
Add
</button>
</div>
))}
{!isWikiSearching && wikiSearchResults.length === 0 ? (
<div style={{ fontSize: 12, color: "#94a3b8" }}>No results.</div>
) : null}
</div>
</div>
) : null}
{searchKind === "geo" && searchQueryDraft.trim().length > 0 ? (
<div style={{ padding: 10, background: "#0b1220", borderRadius: 8, border: "1px solid #1f2937" }}>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
<div style={{ fontWeight: 700, fontSize: 13, color: "white" }}>Geo Results</div>
<div style={{ fontSize: 12, color: "#94a3b8" }}>
{isGeoSearching ? "Searching…" : `${geoSearchResults.length} entities`}
</div>
</div>
<div style={{ marginTop: 8, display: "grid", gap: 8 }}>
{geoSearchResults.slice(0, 6).map((item) => (
<div
key={item.entity_id}
style={{
padding: 8,
borderRadius: 6,
border: "1px solid #1f2937",
background: "transparent",
display: "grid",
gap: 6,
}}
>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
<div style={{ minWidth: 0 }}>
<div style={{ color: "#e5e7eb", fontSize: 12, fontWeight: 700, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{item.name?.trim() || item.entity_id}
</div>
<div style={{ color: "#94a3b8", fontSize: 11, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
{item.entity_id}
</div>
</div>
<div style={{ fontSize: 12, color: "#94a3b8", flex: "0 0 auto" }}>
{Array.isArray(item.geometries) ? item.geometries.length : 0} geos
</div>
</div>
{item.description?.trim() ? (
<div style={{ color: "#cbd5e1", fontSize: 12, lineHeight: 1.35 }}>
{item.description.trim()}
</div>
) : null}
{Array.isArray(item.geometries) && item.geometries.length ? (
<div style={{ display: "grid", gap: 6, maxHeight: 200, overflowY: "auto", paddingRight: 4 }}>
{item.geometries.map((geo) => (
<div
key={geo.id}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
padding: 8,
borderRadius: 6,
border: "1px solid #243244",
background: "#0f172a",
}}
>
<div style={{ minWidth: 0 }}>
<div style={{ color: "#e5e7eb", fontSize: 12, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
#{geo.id}
</div>
<div style={{ color: "#94a3b8", fontSize: 11 }}>
type: {geo.type || "unknown"}{" "}
{geo.time_start != null || geo.time_end != null
? `| time: ${geo.time_start ?? "?"}${geo.time_end ?? "?"}`
: ""}
</div>
</div>
<button
type="button"
onClick={() => handleImportGeoFromSearch(item, geo)}
style={{
border: "none",
background: "#111827",
color: "#93c5fd",
cursor: "pointer",
borderRadius: 6,
padding: "6px 8px",
fontSize: 12,
fontWeight: 700,
flex: "0 0 auto",
}}
title="Import geometry into current editor draft"
>
Import
</button>
</div>
))}
</div>
) : (
<div style={{ fontSize: 12, color: "#94a3b8" }}>
No geometry linked.
</div>
)}
</div>
))}
{!isGeoSearching && geoSearchResults.length === 0 ? (
<div style={{ fontSize: 12, color: "#94a3b8" }}>No results.</div>
) : null}
</div>
</div>
) : null}
<GeometryBindingPanel
geometries={geometryChoices}
selectedGeometryId={selectedFeature ? String(selectedFeature.properties.id) : null}
selectedGeometryBindingIds={selectedGeometryBindingIds}
onToggleBindGeometryForSelectedGeometry={handleToggleBindGeometryForSelectedGeometry}
onFocusGeometry={handleFocusGeometryFromBindingPanel}
statusText={geoBindingStatus}
bindingFilterEnabled={geometryBindingFilterEnabled}
onBindingFilterEnabledChange={setGeometryBindingFilterEnabled}
/>
<ProjectEntityRefsPanel
entityRefs={snapshotEntitiesVisible}
entityForm={entityForm}
onEntityFormChange={handleEntityFormChange}
isEntitySubmitting={isEntitySubmitting}
onCreateEntityOnly={handleCreateEntityOnly}
onUpdateEntity={handleUpdateEntityInProject}
entityFormStatus={entityFormStatus}
hasSelectedGeometry={Boolean(selectedFeature)}
selectedGeometryEntityIds={selectedGeometryEntityIds}
onToggleBindEntityForSelectedGeometry={handleToggleBindEntityForSelectedGeometry}
/>
<WikiSidebarPanel
projectId={projectId}
wikis={snapshotWikis}
setWikis={setSnapshotWikisUndoable}
autoOpen={autoOpenWiki}
requestedActiveId={requestedActiveWikiId}
/>
<EntityWikiBindingsPanel
entities={projectEntityChoices}
wikis={snapshotWikis}
links={snapshotEntityWikiLinks}
setLinks={setSnapshotEntityWikiLinksUndoable}
/>
{!wikiOnly && selectedFeature ? (
<SelectedGeometryPanel
selectedFeatures={selectedFeatures}
entityTypeOptions={GEOMETRY_TYPE_OPTIONS}
geometryMetaForm={geometryMetaForm}
onGeometryMetaFormChange={handleGeometryMetaFormChange}
isEntitySubmitting={isEntitySubmitting}
onApplyGeometryMetadata={featureCommands.applyGeometryMetadata}
changeCount={editor.changeCount}
/>
) : null}
</div> </div>
) : null} }
<GeometryBindingPanel />
geometries={geometryChoices} </>
selectedGeometryId={selectedFeature ? String(selectedFeature.properties.id) : null} )}
selectedGeometryBindingIds={selectedGeometryBindingIds}
onToggleBindGeometryForSelectedGeometry={handleToggleBindGeometryForSelectedGeometry}
onFocusGeometry={handleFocusGeometryFromBindingPanel}
statusText={geoBindingStatus}
bindingFilterEnabled={geometryBindingFilterEnabled}
onBindingFilterEnabledChange={setGeometryBindingFilterEnabled}
/>
<ProjectEntityRefsPanel
entityRefs={snapshotEntitiesVisible}
entityForm={entityForm}
onEntityFormChange={handleEntityFormChange}
isEntitySubmitting={isEntitySubmitting}
onCreateEntityOnly={handleCreateEntityOnly}
onUpdateEntity={handleUpdateEntityInProject}
entityFormStatus={entityFormStatus}
hasSelectedGeometry={Boolean(selectedFeature)}
selectedGeometryEntityIds={selectedGeometryEntityIds}
onToggleBindEntityForSelectedGeometry={handleToggleBindEntityForSelectedGeometry}
/>
<WikiSidebarPanel
projectId={projectId}
wikis={snapshotWikis}
setWikis={setSnapshotWikisUndoable}
autoOpen={autoOpenWiki}
requestedActiveId={requestedActiveWikiId}
/>
<EntityWikiBindingsPanel
entities={projectEntityChoices}
wikis={snapshotWikis}
links={snapshotEntityWikiLinks}
setLinks={setSnapshotEntityWikiLinksUndoable}
/>
{!wikiOnly && selectedFeature ? (
<SelectedGeometryPanel
selectedFeatures={selectedFeatures}
entityTypeOptions={GEOMETRY_TYPE_OPTIONS}
geometryMetaForm={geometryMetaForm}
onGeometryMetaFormChange={handleGeometryMetaFormChange}
isEntitySubmitting={isEntitySubmitting}
onApplyGeometryMetadata={featureCommands.applyGeometryMetadata}
changeCount={editor.changeCount}
/>
) : null}
</div>
}
/>
</div> </div>
); );
} }
+84 -1
View File
@@ -26,6 +26,7 @@ type MapProps = {
geometryVisibility?: Record<string, boolean>; geometryVisibility?: Record<string, boolean>;
selectedFeatureIds: (string | number)[]; selectedFeatureIds: (string | number)[];
onSelectFeatureIds: (ids: (string | number)[]) => void; onSelectFeatureIds: (ids: (string | number)[]) => void;
onSetMode?: (mode: EditorMode, featureId?: string | number) => void;
labelContextDraft?: FeatureCollection; labelContextDraft?: FeatureCollection;
onCreateFeature?: (feature: FeatureCollection["features"][number]) => void; onCreateFeature?: (feature: FeatureCollection["features"][number]) => void;
onDeleteFeature?: (id: string | number) => void; onDeleteFeature?: (id: string | number) => void;
@@ -40,10 +41,13 @@ type MapProps = {
focusFeatureCollection?: FeatureCollection | null; focusFeatureCollection?: FeatureCollection | null;
focusRequestKey?: string | number | null; focusRequestKey?: string | number | null;
focusPadding?: number | import("maplibre-gl").PaddingOptions; focusPadding?: number | import("maplibre-gl").PaddingOptions;
hideOutside?: boolean;
onToggleHideOutside?: () => void;
}; };
export default function Map({ export default function Map({
mode, mode,
onSetMode,
draft, draft,
backgroundVisibility, backgroundVisibility,
geometryVisibility, geometryVisibility,
@@ -63,10 +67,13 @@ export default function Map({
focusFeatureCollection = null, focusFeatureCollection = null,
focusRequestKey = null, focusRequestKey = null,
focusPadding, focusPadding,
hideOutside = false,
onToggleHideOutside,
}: MapProps) { }: MapProps) {
const modeRef = useRef<MapProps["mode"]>(mode); const modeRef = useRef<MapProps["mode"]>(mode);
const draftRef = useRef<FeatureCollection>(draft); const draftRef = useRef<FeatureCollection>(draft);
const onSelectFeatureIdsRef = useRef(onSelectFeatureIds); const onSelectFeatureIdsRef = useRef(onSelectFeatureIds);
const onSetModeRef = useRef(onSetMode);
const onHoverFeatureChangeRef = useRef<MapProps["onHoverFeatureChange"]>(onHoverFeatureChange); const onHoverFeatureChangeRef = useRef<MapProps["onHoverFeatureChange"]>(onHoverFeatureChange);
const onCreateRef = useRef<MapProps["onCreateFeature"]>(onCreateFeature); const onCreateRef = useRef<MapProps["onCreateFeature"]>(onCreateFeature);
const onDeleteRef = useRef<MapProps["onDeleteFeature"]>(onDeleteFeature); const onDeleteRef = useRef<MapProps["onDeleteFeature"]>(onDeleteFeature);
@@ -75,6 +82,7 @@ export default function Map({
useEffect(() => { modeRef.current = mode; }, [mode]); useEffect(() => { modeRef.current = mode; }, [mode]);
useEffect(() => { draftRef.current = draft; }, [draft]); useEffect(() => { draftRef.current = draft; }, [draft]);
useEffect(() => { onSelectFeatureIdsRef.current = onSelectFeatureIds; }, [onSelectFeatureIds]); useEffect(() => { onSelectFeatureIdsRef.current = onSelectFeatureIds; }, [onSelectFeatureIds]);
useEffect(() => { onSetModeRef.current = onSetMode; }, [onSetMode]);
useEffect(() => { onHoverFeatureChangeRef.current = onHoverFeatureChange; }, [onHoverFeatureChange]); useEffect(() => { onHoverFeatureChangeRef.current = onHoverFeatureChange; }, [onHoverFeatureChange]);
useEffect(() => { onCreateRef.current = onCreateFeature; }, [onCreateFeature]); useEffect(() => { onCreateRef.current = onCreateFeature; }, [onCreateFeature]);
useEffect(() => { onDeleteRef.current = onDeleteFeature; }, [onDeleteFeature]); useEffect(() => { onDeleteRef.current = onDeleteFeature; }, [onDeleteFeature]);
@@ -106,6 +114,7 @@ export default function Map({
allowGeometryEditing, allowGeometryEditing,
selectedFeatureIds, selectedFeatureIds,
onSelectFeatureIdsRef, onSelectFeatureIdsRef,
onSetModeRef,
onCreateRef, onCreateRef,
onDeleteRef, onDeleteRef,
onUpdateRef, onUpdateRef,
@@ -150,6 +159,14 @@ export default function Map({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMapLoaded]); }, [isMapLoaded]);
useEffect(() => {
const map = mapRef.current;
if (map && isMapLoaded) {
// Trigger resize after a short delay to allow layout to settle
setTimeout(() => map.resize(), 100);
}
}, [mode, isMapLoaded]);
return ( return (
<div style={{ width: "100%", height, position: "relative" }}> <div style={{ width: "100%", height, position: "relative" }}>
<div ref={containerRef} style={{ width: "100%", height: "100%" }} /> <div ref={containerRef} style={{ width: "100%", height: "100%" }} />
@@ -198,7 +215,7 @@ export default function Map({
> >
<div <div
style={{ style={{
maxWidth: "520px", maxWidth: "650px",
margin: "0 auto", margin: "0 auto",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
@@ -212,6 +229,72 @@ export default function Map({
pointerEvents: "auto", pointerEvents: "auto",
}} }}
> >
{mode === "replay" && (
<>
<button
type="button"
onClick={() => onSetMode?.("select")}
style={{
...zoomButtonStyle,
width: "auto",
padding: "0 12px",
fontSize: "12px",
fontWeight: 700,
background: "#7f1d1d",
color: "white",
border: "1px solid #991b1b",
borderRadius: "999px",
cursor: "pointer",
marginRight: "4px",
}}
>
Thoát Replay Edit
</button>
<div
onClick={onToggleHideOutside}
style={{
display: "flex",
alignItems: "center",
gap: "8px",
cursor: "pointer",
marginRight: "8px",
userSelect: "none",
}}
>
<span style={{ fontSize: "12px", fontWeight: 700, color: hideOutside ? "#fb7185" : "#94a3b8" }}>
Hide Outside
</span>
<div
style={{
width: "32px",
height: "18px",
borderRadius: "10px",
background: hideOutside ? "#e11d48" : "#334155",
position: "relative",
transition: "background 0.2s",
border: "1px solid rgba(255,255,255,0.1)",
}}
>
<div
style={{
position: "absolute",
top: "2px",
left: hideOutside ? "16px" : "2px",
width: "12px",
height: "12px",
borderRadius: "50%",
background: "white",
transition: "left 0.2s cubic-bezier(0.4, 0, 0.2, 1)",
boxShadow: "0 1px 2px rgba(0,0,0,0.3)",
}}
/>
</div>
</div>
<div style={{ width: "1px", height: "20px", background: "rgba(148, 163, 184, 0.3)", marginRight: "4px" }} />
</>
)}
<label <label
title={ title={
isGlobeProjection isGlobeProjection
+7
View File
@@ -36,5 +36,12 @@ export function ModeHint({ mode }: { mode: EditorMode }) {
</div> </div>
) )
} }
if (mode === "replay") {
return (
<div style={{ marginTop: 6, fontSize: 12, color: "#93c5fd" }}>
Đang trong chế đ trình diễn diễn biến kịch bản.
</div>
)
}
return null; return null;
} }
+5
View File
@@ -156,8 +156,13 @@ export function filterDraftByGeometryVisibility(
return { return {
...fc, ...fc,
features: fc.features.filter((feature) => { features: fc.features.filter((feature) => {
const id = String(feature.properties.id);
// Kiểm tra ẩn theo ID cụ thể (ưu tiên cao nhất)
if (visibility[id] === false) return false;
const key = getFeatureSemanticType(feature); const key = getFeatureSemanticType(feature);
if (!key) return true; if (!key) return true;
// Kiểm tra ẩn theo loại (semantic type)
return visibility[key] !== false; return visibility[key] !== false;
}), }),
}; };
+5 -1
View File
@@ -26,6 +26,7 @@ type UseMapInteractionProps = {
allowGeometryEditing: boolean; allowGeometryEditing: boolean;
selectedFeatureIds: (string | number)[]; selectedFeatureIds: (string | number)[];
onSelectFeatureIdsRef: React.MutableRefObject<(ids: (string | number)[]) => void>; onSelectFeatureIdsRef: React.MutableRefObject<(ids: (string | number)[]) => void>;
onSetModeRef: React.MutableRefObject<((mode: EditorMode) => void) | undefined>;
onCreateRef: React.MutableRefObject<((feature: FeatureCollection["features"][number]) => void) | undefined>; onCreateRef: React.MutableRefObject<((feature: FeatureCollection["features"][number]) => void) | undefined>;
onDeleteRef: React.MutableRefObject<((id: string | number) => void) | undefined>; onDeleteRef: React.MutableRefObject<((id: string | number) => void) | undefined>;
onUpdateRef: React.MutableRefObject<((id: string | number, geometry: Geometry) => void) | undefined>; onUpdateRef: React.MutableRefObject<((id: string | number, geometry: Geometry) => void) | undefined>;
@@ -40,6 +41,7 @@ export function useMapInteraction({
allowGeometryEditing, allowGeometryEditing,
selectedFeatureIds, selectedFeatureIds,
onSelectFeatureIdsRef, onSelectFeatureIdsRef,
onSetModeRef,
onCreateRef, onCreateRef,
onDeleteRef, onDeleteRef,
onUpdateRef, onUpdateRef,
@@ -142,7 +144,8 @@ export function useMapInteraction({
editingEngineRef.current?.beginEditing((originalFeature || feature) as any); editingEngineRef.current?.beginEditing((originalFeature || feature) as any);
} }
: undefined, : undefined,
(ids) => onSelectFeatureIdsRef.current?.(ids) (ids) => onSelectFeatureIdsRef.current?.(ids),
(id: string | number) => onSetModeRef.current?.("replay", id)
); );
const cleanupPoint = initPoint( const cleanupPoint = initPoint(
@@ -236,6 +239,7 @@ export function useMapInteraction({
engineBindingsRef.current = { engineBindingsRef.current = {
draw: drawingEngine, draw: drawingEngine,
select: selectEngine, select: selectEngine,
replay: selectEngine,
"add-line": lineEngine, "add-line": lineEngine,
"add-path": pathEngine, "add-path": pathEngine,
"add-circle": circleEngine, "add-circle": circleEngine,
+1 -1
View File
@@ -11,7 +11,7 @@ import {
} from "@/uhm/lib/map/styles/style"; } from "@/uhm/lib/map/styles/style";
import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants"; import { EMPTY_FEATURE_COLLECTION } from "@/uhm/lib/map/geo/constants";
import { PATH_ARROW_ICON_ID, PATH_ARROW_SOURCE_ID, POLYGON_LABEL_SOURCE_ID } from "@/uhm/lib/map/constants"; import { PATH_ARROW_ICON_ID, PATH_ARROW_SOURCE_ID, POLYGON_LABEL_SOURCE_ID } from "@/uhm/lib/map/constants";
import { ensurePointGeotypeIcons, getAllGeotypeLabelLayers, getAllGeotypeLayers } from "@/uhm/lib/map/styles/geotypes"; import { ensurePointGeotypeIcons, getAllGeotypeLabelLayers, getAllGeotypeLayers } from "@/uhm/lib/map/styles/geotypeLayers";
import { import {
applyBackgroundLayerVisibility, applyBackgroundLayerVisibility,
buildTypeMatchExpression, buildTypeMatchExpression,
+2 -1
View File
@@ -7,7 +7,8 @@ export type EditorMode =
| "add-point" | "add-point"
| "add-line" | "add-line"
| "add-path" | "add-path"
| "add-circle"; | "add-circle"
| "replay";
export type TimelineRange = { export type TimelineRange = {
min: number; min: number;
+18 -5
View File
@@ -7,7 +7,8 @@ export function initSelect(
getMode: ModeGetter, getMode: ModeGetter,
onDelete?: (id: string | number) => void, onDelete?: (id: string | number) => void,
onEdit?: (feature: maplibregl.MapGeoJSONFeature) => void, onEdit?: (feature: maplibregl.MapGeoJSONFeature) => void,
onSelectIds?: (ids: (string | number)[]) => void onSelectIds?: (ids: (string | number)[]) => void,
onReplayEdit?: (id: string | number) => void
) { ) {
const FEATURE_STATE_SOURCES = [ const FEATURE_STATE_SOURCES = [
@@ -16,7 +17,7 @@ export function initSelect(
"path-arrow-shapes", "path-arrow-shapes",
] as const; ] as const;
const selectedIds = new Set<number | string>(); const selectedIds = new Set<number | string>();
const hasContextActions = Boolean(onDelete || onEdit); const hasContextActions = Boolean(onDelete || onEdit || onReplayEdit);
let contextMenu: HTMLDivElement | null = null; let contextMenu: HTMLDivElement | null = null;
let docClickHandler: ((ev: MouseEvent) => void) | null = null; let docClickHandler: ((ev: MouseEvent) => void) | null = null;
@@ -54,7 +55,7 @@ export function initSelect(
// Chọn feature theo click trái, hỗ trợ additive bằng Alt. // Chọn feature theo click trái, hỗ trợ additive bằng Alt.
function onClick(e: maplibregl.MapLayerMouseEvent) { function onClick(e: maplibregl.MapLayerMouseEvent) {
if (getMode() !== "select") return; if (getMode() !== "select" && getMode() !== "replay") return;
const selectableLayers = getSelectableLayers(); const selectableLayers = getSelectableLayers();
if (!selectableLayers.length) return; if (!selectableLayers.length) return;
@@ -74,11 +75,12 @@ export function initSelect(
// Hiển thị menu ngữ cảnh (sửa/xóa) khi click chuột phải. // Hiển thị menu ngữ cảnh (sửa/xóa) khi click chuột phải.
// Mở menu thao tác khi click phải lên feature. // Mở menu thao tác khi click phải lên feature.
function onRightClick(e: maplibregl.MapLayerMouseEvent) { function onRightClick(e: maplibregl.MapLayerMouseEvent) {
if (getMode() !== "select") return; if (getMode() !== "select" && getMode() !== "replay") return;
const selectableLayers = getSelectableLayers(); const selectableLayers = getSelectableLayers();
if (!selectableLayers.length) return; if (!selectableLayers.length) return;
e.preventDefault(); // block browser menu e.preventDefault(); // block browser menu
if (getMode() === "replay") return;
const features = map.queryRenderedFeatures(e.point, { const features = map.queryRenderedFeatures(e.point, {
layers: selectableLayers, layers: selectableLayers,
@@ -105,7 +107,7 @@ export function initSelect(
// Đổi cursor pointer khi hover lên đối tượng có thể chọn. // Đổi cursor pointer khi hover lên đối tượng có thể chọn.
function onMove(e: maplibregl.MapLayerMouseEvent) { function onMove(e: maplibregl.MapLayerMouseEvent) {
if (getMode() !== "select") return; if (getMode() !== "select" && getMode() !== "replay") return;
const selectableLayers = getSelectableLayers(); const selectableLayers = getSelectableLayers();
if (!selectableLayers.length) { if (!selectableLayers.length) {
map.getCanvas().style.cursor = ""; map.getCanvas().style.cursor = "";
@@ -218,6 +220,17 @@ export function initSelect(
hasMenuItems = true; hasMenuItems = true;
} }
if (
selectedCount === 1 &&
onReplayEdit
) {
const featureId = clickedFeature.id ?? clickedFeature.properties?.id;
if (featureId) {
menu.appendChild(createItem("Replay Edit", () => onReplayEdit(featureId)));
hasMenuItems = true;
}
}
if (onDelete) { if (onDelete) {
menu.appendChild( menu.appendChild(
createItem( createItem(
@@ -1,36 +1,36 @@
import maplibregl from "maplibre-gl"; import maplibregl from "maplibre-gl";
export const TYPE_MATCH_EXPR: maplibregl.ExpressionSpecification = ["coalesce", ["get", "type"], ["get", "entity_type_id"], ""]; export const TYPE_MATCH_EXPR: maplibregl.ExpressionSpecification = ["coalesce", ["get", "type"], ["get", "entity_type_id"], ""];
export { ensurePointGeotypeIcons } from "./pointStyle"; export { ensurePointGeotypeIcons } from "./shared/pointStyle";
import { getDefenseLineLayers } from "./defense_line"; import { getDefenseLineLayers } from "./geotypes/defense_line";
import { getAttackRouteLayers } from "./attack_route"; import { getAttackRouteLayers } from "./geotypes/attack_route";
import { getRetreatRouteLayers } from "./retreat_route"; import { getRetreatRouteLayers } from "./geotypes/retreat_route";
import { getInvasionRouteLayers } from "./invasion_route"; import { getInvasionRouteLayers } from "./geotypes/invasion_route";
import { getMigrationRouteLayers } from "./migration_route"; import { getMigrationRouteLayers } from "./geotypes/migration_route";
import { getRefugeeRouteLayers } from "./refugee_route"; import { getRefugeeRouteLayers } from "./geotypes/refugee_route";
import { getTradeRouteLayers } from "./trade_route"; import { getTradeRouteLayers } from "./geotypes/trade_route";
import { getShippingRouteLayers } from "./shipping_route"; import { getShippingRouteLayers } from "./geotypes/shipping_route";
import { getCountryLayers } from "./country"; import { getCountryLayers } from "./geotypes/country";
import { getStateLayers } from "./state"; import { getStateLayers } from "./geotypes/state";
import { getEmpireLayers } from "./empire"; import { getEmpireLayers } from "./geotypes/empire";
import { getKingdomLayers } from "./kingdom"; import { getKingdomLayers } from "./geotypes/kingdom";
import { getWarLayers } from "./war"; import { getWarLayers } from "./geotypes/war";
import { getBattleLayers } from "./battle"; import { getBattleLayers } from "./geotypes/battle";
import { getCivilizationLayers } from "./civilization"; import { getCivilizationLayers } from "./geotypes/civilization";
import { getRebellionZoneLayers } from "./rebellion_zone"; import { getRebellionZoneLayers } from "./geotypes/rebellion_zone";
import { getPersonDeathplaceLayers } from "./person_deathplace"; import { getPersonDeathplaceLayers } from "./geotypes/person_deathplace";
import { getPersonBirthplaceLayers } from "./person_birthplace"; import { getPersonBirthplaceLayers } from "./geotypes/person_birthplace";
import { getPersonActivityLayers } from "./person_activity"; import { getPersonActivityLayers } from "./geotypes/person_activity";
import { getTempleLayers } from "./temple"; import { getTempleLayers } from "./geotypes/temple";
import { getCapitalLayers } from "./capital"; import { getCapitalLayers } from "./geotypes/capital";
import { getCityLayers } from "./city"; import { getCityLayers } from "./geotypes/city";
import { getFortressLayers } from "./fortress"; import { getFortressLayers } from "./geotypes/fortress";
import { getCastleLayers } from "./castle"; import { getCastleLayers } from "./geotypes/castle";
import { getRuinLayers } from "./ruin"; import { getRuinLayers } from "./geotypes/ruin";
import { getPortLayers } from "./port"; import { getPortLayers } from "./geotypes/port";
import { getBridgeLayers } from "./bridge"; import { getBridgeLayers } from "./geotypes/bridge";
import { getLineLabelLayers } from "./lineLabels"; import { getLineLabelLayers } from "./shared/lineLabels";
import { getPolygonLabelLayers } from "./polygonLabels"; import { getPolygonLabelLayers } from "./shared/polygonLabels";
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "./styleBuilders"; import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getAttackRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getAttackRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId; void pointSourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "./styleBuilders"; import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getBattleLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getBattleLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId; void pathArrowSourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getBridgeLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getBridgeLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getCapitalLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getCapitalLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getCastleLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getCastleLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getCityLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getCityLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "./styleBuilders"; import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getCivilizationLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getCivilizationLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId; void pathArrowSourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "./styleBuilders"; import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getCountryLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getCountryLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId; void pathArrowSourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "./styleBuilders"; import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getDefenseLineLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getDefenseLineLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId; void pointSourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "./styleBuilders"; import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getEmpireLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getEmpireLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId; void pathArrowSourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getFortressLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getFortressLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "./styleBuilders"; import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getInvasionRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getInvasionRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId; void pointSourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "./styleBuilders"; import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getKingdomLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getKingdomLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId; void pathArrowSourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "./styleBuilders"; import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getMigrationRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getMigrationRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId; void pointSourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getPersonActivityLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getPersonActivityLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getPersonBirthplaceLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getPersonBirthplaceLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getPersonDeathplaceLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getPersonDeathplaceLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getPortLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getPortLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "./styleBuilders"; import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getRebellionZoneLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getRebellionZoneLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId; void pathArrowSourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "./styleBuilders"; import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getRefugeeRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getRefugeeRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId; void pointSourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "./styleBuilders"; import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getRetreatRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getRetreatRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId; void pointSourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getRuinLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getRuinLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "./styleBuilders"; import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getShippingRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getShippingRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId; void pointSourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "./styleBuilders"; import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getStateLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getStateLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId; void pathArrowSourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "./pointStyle"; import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getTempleLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getTempleLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId; void sourceId;
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "./styleBuilders"; import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getTradeRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getTradeRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId; void pointSourceId;
+1 -1
View File
@@ -1,5 +1,5 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "./styleBuilders"; import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getWarLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getWarLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId; void pathArrowSourceId;
+48
View File
@@ -15,6 +15,54 @@ export type CommitSnapshot = {
geometry_entity: GeometryEntitySnapshot[]; geometry_entity: GeometryEntitySnapshot[];
wikis: WikiSnapshot[]; wikis: WikiSnapshot[];
entity_wiki: EntityWikiLinkSnapshot[]; entity_wiki: EntityWikiLinkSnapshot[];
replays?: BattleReplay[];
};
// ---- Replay / Scripting System ----
export type UIFunctionName =
| "hide_timeline"
| "hide_layer_panel"
| "hide_wiki_panel"
| "hide_zoom_panel"
| "hide_all_UI"
| "open_wiki";
export type MapFunctionName =
| "zoom_to_lnglat"
| "zoom_scale"
| "zoom_geometries"
| "change_geometry_color"
| "change_geometries_color"
| "change_geometry_texture"
| "change_geometries_texture"
| "hide_geometries";
export type NarrativeFunctionName = "set_title" | "set_descriptions";
export type ReplayAction<T> = {
function_name: T;
params: any[];
};
export type ReplayStep = {
duration: number; // Trọng số thời gian của step trong 1 stage
use_UI_function: ReplayAction<UIFunctionName>[];
use_map_function: ReplayAction<MapFunctionName>[];
use_narrow_function: ReplayAction<NarrativeFunctionName>[];
};
export type ReplayStage = {
id: number; // số đếm thứ tự từ 0
title?: string;
detail_time_start: string;
detail_time_stop: string;
steps: ReplayStep[];
};
export type BattleReplay = {
geometry_id: string; // geometry mà khi nhấn vào là có thể replay
detail: ReplayStage[];
}; };
// ---- GeoJSON / FeatureCollection ---- // ---- GeoJSON / FeatureCollection ----