init: replay mode
This commit is contained in:
+390
-328
@@ -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 {
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
+30
-30
@@ -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,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,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,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,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,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,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,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,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,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,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,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,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,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,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;
|
||||||
|
|||||||
@@ -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 ----
|
||||||
|
|||||||
Reference in New Issue
Block a user