refactor: consolidate redundant geo-types and implement legacy key mapping in GeoTypeMap

This commit is contained in:
taDuc
2026-05-24 10:36:45 +07:00
parent 82dfd7fa56
commit c8d2415e50
32 changed files with 401 additions and 587 deletions
+228 -152
View File
@@ -495,7 +495,7 @@ function EditorPageContent() {
initialMapViewState: previewSession?.mapViewState ?? null, initialMapViewState: previewSession?.mapViewState ?? null,
selectedStageId: previewSession?.selectedStageId ?? replaySelection.stageId, selectedStageId: previewSession?.selectedStageId ?? replaySelection.stageId,
selectedStepIndex: previewSession?.selectedStepIndex ?? replaySelection.stepIndex, selectedStepIndex: previewSession?.selectedStepIndex ?? replaySelection.stepIndex,
onSelectStep: () => {}, onSelectStep: () => { },
}); });
const { const {
hiddenGeometryIds: replayPreviewHiddenGeometryIds, hiddenGeometryIds: replayPreviewHiddenGeometryIds,
@@ -788,7 +788,7 @@ function EditorPageContent() {
// QUY TẮC: Geo chọn đầu tiên là geo main. // QUY TẮC: Geo chọn đầu tiên là geo main.
const finalSelectedIds = Array.from(new Set([...selectedFeatureIds, featureId])); const finalSelectedIds = Array.from(new Set([...selectedFeatureIds, featureId]));
const triggerId = selectedFeatureIds.length > 0 ? selectedFeatureIds[0] : featureId; const triggerId = selectedFeatureIds.length > 0 ? selectedFeatureIds[0] : featureId;
setReplayFeatureId(triggerId); setReplayFeatureId(triggerId);
setReplaySelection({ stageId: null, stepIndex: null }); setReplaySelection({ stageId: null, stepIndex: null });
editor.switchReplayContext(triggerId, finalSelectedIds); editor.switchReplayContext(triggerId, finalSelectedIds);
@@ -997,7 +997,7 @@ function EditorPageContent() {
if (isReplayEditMode && hideOutside) { if (isReplayEditMode && hideOutside) {
// Trong mode replay, ta chỉ hiển thị những gì có trong draft của replay đó // Trong mode replay, ta chỉ hiển thị những gì có trong draft của replay đó
const currentReplayFeatureIds = new Set(editor.draft.features.map(f => String(f.properties.id))); const currentReplayFeatureIds = new Set(editor.draft.features.map(f => String(f.properties.id)));
// Ẩn tất cả các geo KHÔNG nằm trong draft replay hiện tại // Ẩn tất cả các geo KHÔNG nằm trong draft replay hiện tại
Object.keys(visibility).forEach(fid => { Object.keys(visibility).forEach(fid => {
if (fid === String(replayFeatureId)) { if (fid === String(replayFeatureId)) {
@@ -1062,7 +1062,7 @@ function EditorPageContent() {
// Xóa pending submission để backend cho phép mở editor lại. // Xóa pending submission để backend cho phép mở editor lại.
const unlockByDeletingPendingSubmission = useCallback(async () => { const unlockByDeletingPendingSubmission = useCallback(async () => {
if (!blockedPendingSubmissionId) return; if (!blockedPendingSubmissionId) return;
const confirmed = window.confirm("Xoa submission PENDING de unlock editor? Hanh dong nay khong the hoan tac."); const confirmed = window.confirm("Bạn chắc chắn muốn xóa Submition? - việc này không làm hỏng project của bạn");
if (!confirmed) return; if (!confirmed) return;
try { try {
setIsOpeningSection(true); setIsOpeningSection(true);
@@ -1599,7 +1599,7 @@ function EditorPageContent() {
} }
const prevBindingIds = normalizeFeatureBindingIds(targetFeature); const prevBindingIds = normalizeFeatureBindingIds(targetFeature);
// Merge prevBindingIds with sourceIds (which are strings of selected features) // Merge prevBindingIds with sourceIds (which are strings of selected features)
// filter out targetId itself (we can't bind a geometry to itself) // filter out targetId itself (we can't bind a geometry to itself)
const newSources = sourceIds.map(String).filter((x) => x !== idStr); const newSources = sourceIds.map(String).filter((x) => x !== idStr);
@@ -1966,6 +1966,127 @@ function EditorPageContent() {
[entities, labelContextBaseDraft] [entities, labelContextBaseDraft]
); );
if (blockedPendingSubmissionId) {
return (
<div style={{ display: "flex", minHeight: "100vh", width: "100vw", background: "#0b1220", color: "white", padding: "40px", alignItems: "center", justifyContent: "center" }}>
<div style={{ maxWidth: 640, width: "100%", background: "#0f172a", border: "1px solid #1e293b", borderRadius: 12, padding: 32, boxShadow: "0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3)" }}>
<div style={{ display: "flex", alignItems: "center", gap: 12, marginBottom: 16 }}>
<svg style={{ width: 28, height: 28, color: "#ef4444" }} fill="none" viewBox="0 0 24 24" stroke="currentColor" width="28" height="28">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m0 0v2m0-2h2m-2 0H8m13 0a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 700 }}>Editor đang bị khóa</h2>
</div>
<div style={{ fontSize: 14, color: "#94a3b8", lineHeight: "1.6" }}>
Project này đang submission trạng thái <b style={{ color: "#ef4444" }}>PENDING</b> (id: <code style={{ color: "#f1f5f9", background: "#1e293b", padding: "2px 6px", borderRadius: 4 }}>{blockedPendingSubmissionId}</code>). Theo quy trình làm việc, khi submission đang pending thì không đưc tạo submission/commit mới không đưc vào editor.
</div>
<div style={{ marginTop: 24, display: "flex", gap: 12 }}>
<button
onClick={unlockByDeletingPendingSubmission}
disabled={isOpeningSection}
style={{
padding: "10px 16px",
borderRadius: 6,
border: "none",
background: isOpeningSection ? "#334155" : "#ef4444",
color: "white",
fontWeight: 600,
fontSize: 14,
cursor: isOpeningSection ? "not-allowed" : "pointer",
transition: "background 0.2s",
}}
onMouseEnter={(e) => { if (!isOpeningSection) e.currentTarget.style.background = "#dc2626"; }}
onMouseLeave={(e) => { if (!isOpeningSection) e.currentTarget.style.background = "#ef4444"; }}
>
Xóa submission pending đ unlock
</button>
<button
onClick={() => router.push("/user/projects")}
style={{
padding: "10px 16px",
borderRadius: 6,
border: "1px solid #334155",
background: "#1e293b",
color: "#f1f5f9",
fontWeight: 600,
fontSize: 14,
cursor: "pointer",
transition: "background 0.2s",
}}
onMouseEnter={(e) => e.currentTarget.style.background = "#334155"}
onMouseLeave={(e) => e.currentTarget.style.background = "#1e293b"}
>
Quay lại danh sách projects
</button>
</div>
</div>
</div>
);
}
if (isOpeningSection || !activeSection) {
return (
<div style={{ display: "flex", flexDirection: "column", minHeight: "100vh", width: "100vw", background: "#0b1220", color: "white", alignItems: "center", justifyContent: "center", gap: "16px" }}>
{!activeSection && !isOpeningSection ? (
<div style={{ maxWidth: 480, textAlign: "center", padding: "20px" }}>
<h2 style={{ fontSize: "18px", fontWeight: "600", marginBottom: "8px", color: "#ef4444" }}>Lỗi tải Project</h2>
<div style={{ fontSize: "14px", color: "#94a3b8", marginBottom: "20px" }}>
{entityStatus || "Không thể tải thông tin dự án. Vui lòng thử lại hoặc quay lại danh sách."}
</div>
<div style={{ display: "flex", gap: "12px", justifyContent: "center" }}>
<button
onClick={openProject}
style={{
padding: "8px 16px",
borderRadius: 6,
background: "#3b82f6",
color: "white",
border: "none",
fontWeight: "600",
cursor: "pointer"
}}
>
Thử lại
</button>
<button
onClick={() => router.push("/user/projects")}
style={{
padding: "8px 16px",
borderRadius: 6,
background: "#1e293b",
color: "#f1f5f9",
border: "1px solid #334155",
fontWeight: "600",
cursor: "pointer"
}}
>
Quay lại
</button>
</div>
</div>
) : (
<>
<div className="premium-spinner" style={{
width: "40px",
height: "40px",
border: "3px solid rgba(255, 255, 255, 0.1)",
borderRadius: "50%",
borderTopColor: "#3b82f6",
animation: "spin 1s linear infinite"
}} />
<style>{`
@keyframes spin {
to { transform: rotate(360deg); }
}
`}</style>
<div style={{ fontSize: "15px", fontWeight: "500", color: "#94a3b8" }}>
Đang tải dữ liệu bản đ...
</div>
</>
)}
</div>
);
}
return ( return (
<div style={{ display: "flex", minHeight: "100vh" }}> <div style={{ display: "flex", minHeight: "100vh" }}>
{!isReplayEditMode && !isReplayPreviewMode ? ( {!isReplayEditMode && !isReplayPreviewMode ? (
@@ -1980,7 +2101,7 @@ function EditorPageContent() {
onRestoreCommit={restoreCommit} onRestoreCommit={restoreCommit}
isSaving={isSaving} isSaving={isSaving}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
sectionTitle={activeSection?.title || "Đang tải project"} sectionTitle={activeSection.title || "Đang tải project"}
projectStatus={projectState?.status || "editing"} projectStatus={projectState?.status || "editing"}
commitTitle={commitTitle} commitTitle={commitTitle}
onCommitTitleChange={setCommitTitle} onCommitTitleChange={setCommitTitle}
@@ -2019,8 +2140,8 @@ function EditorPageContent() {
previewPlaybackSpeed={1} previewPlaybackSpeed={1}
onPlayPreviewFromStart={() => openReplayPreview("start")} onPlayPreviewFromStart={() => openReplayPreview("start")}
onPlayPreviewFromSelection={() => openReplayPreview("selection")} onPlayPreviewFromSelection={() => openReplayPreview("selection")}
onStopPreview={() => {}} onStopPreview={() => { }}
onResetPreview={() => {}} onResetPreview={() => { }}
/> />
<ResizeHandle <ResizeHandle
title="Resize left panel" title="Resize left panel"
@@ -2031,158 +2152,113 @@ function EditorPageContent() {
</> </>
) : null} ) : null}
{blockedPendingSubmissionId ? ( <div style={{ flex: 1, position: "relative", minHeight: "100vh" }}>
<div style={{ flex: 1, minHeight: "100vh", background: "#0b1220", color: "white", padding: "24px" }}> {isBackgroundVisibilityReady ? (
<div style={{ maxWidth: 720 }}> <Map
<h2 style={{ margin: 0, fontSize: 18, fontWeight: 700 }}>Editor dang bi khoa</h2> ref={mapHandleRef}
<div style={{ marginTop: 10, fontSize: 13, color: "#cbd5e1" }}> mode={mode}
Project nay dang co submission o trang thai <b>PENDING</b> (id:{" "} onSetMode={setMode}
<code style={{ color: "white" }}>{blockedPendingSubmissionId}</code>). Theo BE moi, khi renderDraft={mapRenderDraft}
submission dang pending thi khong duoc tao submission/commit moi va khong duoc vao editor. labelContextDraft={mapLabelContextDraft}
</div> labelTimelineYear={activeTimelineFilterEnabled ? activeTimelineYear : null}
<div style={{ marginTop: 14, display: "flex", gap: 10, alignItems: "center" }}> selectedFeatureIds={selectedFeatureIds}
<button onSelectFeatureIds={setSelectedFeatureIds}
onClick={unlockByDeletingPendingSubmission} onCreateFeature={handleCreateFeature}
disabled={isOpeningSection} onDeleteFeature={(id) => {
style={{ if (Array.isArray(id)) {
padding: "10px 12px", editor.deleteFeatures(id);
borderRadius: 6, } else {
border: "1px solid #334155", editor.deleteFeature(id);
background: isOpeningSection ? "#334155" : "#ef4444",
color: "white",
cursor: isOpeningSection ? "not-allowed" : "pointer",
}}
>
Xoa submission pending de unlock
</button>
<button
onClick={() => router.push("/user/projects")}
style={{
padding: "10px 12px",
borderRadius: 6,
border: "1px solid #334155",
background: "#111827",
color: "white",
cursor: "pointer",
}}
>
Quay lai danh sach projects
</button>
</div>
</div>
</div>
) : null}
{!blockedPendingSubmissionId ? (
<div style={{ flex: 1, position: "relative", minHeight: "100vh" }}>
{isBackgroundVisibilityReady ? (
<Map
ref={mapHandleRef}
mode={mode}
onSetMode={setMode}
renderDraft={mapRenderDraft}
labelContextDraft={mapLabelContextDraft}
labelTimelineYear={activeTimelineFilterEnabled ? activeTimelineYear : null}
selectedFeatureIds={selectedFeatureIds}
onSelectFeatureIds={setSelectedFeatureIds}
onCreateFeature={handleCreateFeature}
onDeleteFeature={(id) => {
if (Array.isArray(id)) {
editor.deleteFeatures(id);
} else {
editor.deleteFeature(id);
}
}}
onHideFeature={handleHideGeometryLocal}
onUpdateFeature={editor.updateFeature}
backgroundVisibility={backgroundVisibility}
geometryVisibility={effectiveGeometryVisibility}
applyGeometryBindingFilter={isReplayEditMode || isReplayPreviewMode ? false : geometryBindingFilterEnabled}
highlightFeatures={null}
focusFeatureCollection={geometryFocusRequest?.collection || null}
focusRequestKey={geometryFocusRequest?.key ?? null}
focusPadding={96}
imageOverlay={imageOverlay}
onImageOverlayChange={setImageOverlay}
onBindGeometries={handleBindGeometries}
/>
) : (
<div style={{ width: "100%", height: "100%", background: "#0b1220" }} />
)}
{isReplayPreviewMode ? (
<ReplayPreviewOverlay
isPreviewMode={true}
isPlaying={replayPreview.isPlaying}
title={replayPreview.title}
descriptions={replayPreview.descriptions}
subtitle={replayPreview.subtitle}
dialog={replayPreview.dialog}
image={replayPreview.image}
toasts={replayPreview.toasts}
sidebarOpen={replayPreview.sidebarOpen}
playbackSpeed={replayPreview.playbackSpeed}
activeStepLabel={replayPreviewActiveStepLabel}
activeStepNumber={replayPreview.activeStepNumber}
totalSteps={replayPreview.totalSteps}
onPlayPreview={replayPreview.playFromStart}
onStopPreview={replayPreview.stopPreview}
onResetPreview={replayPreview.resetPreview}
onExitPreview={exitReplayPreview}
/>
) : null}
{isReplayPreviewMode && replayPreview.sidebarOpen ? (
<aside
style={{
position: "absolute",
top: 16,
right: 16,
bottom: 16,
width: 420,
maxWidth: "calc(100vw - 2rem)",
zIndex: 16,
}}
>
<PublicWikiSidebar
entity={null}
wiki={replayPreviewActiveWiki}
isLoading={isPreviewWikiLoading}
error={replayPreview.activeWikiId ? previewWikiError : "Chưa có wiki được chọn trong step này."}
onClose={() => {
setPreviewWikiError(null);
replayPreview.closeWikiPanel();
}}
onWikiLinkRequest={handleReplayPreviewWikiLinkRequest}
/>
</aside>
) : null}
{!isReplayPreviewMode || replayPreview.timelineVisible ? (
<TimelineBar
year={activeTimelineYear}
onYearChange={
isReplayPreviewMode
? replayPreview.setTimelineYear
: handleTimelineYearChange
}
isLoading={false}
disabled={false}
statusText={null}
filterEnabled={activeTimelineFilterEnabled}
onFilterEnabledChange={
isReplayPreviewMode
? replayPreview.setTimelineFilterEnabled
: setTimelineFilterEnabled
} }
}}
onHideFeature={handleHideGeometryLocal}
onUpdateFeature={editor.updateFeature}
backgroundVisibility={backgroundVisibility}
geometryVisibility={effectiveGeometryVisibility}
applyGeometryBindingFilter={isReplayEditMode || isReplayPreviewMode ? false : geometryBindingFilterEnabled}
highlightFeatures={null}
focusFeatureCollection={geometryFocusRequest?.collection || null}
focusRequestKey={geometryFocusRequest?.key ?? null}
focusPadding={96}
imageOverlay={imageOverlay}
onImageOverlayChange={setImageOverlay}
onBindGeometries={handleBindGeometries}
/>
) : (
<div style={{ width: "100%", height: "100%", background: "#0b1220" }} />
)}
{isReplayPreviewMode ? (
<ReplayPreviewOverlay
isPreviewMode={true}
isPlaying={replayPreview.isPlaying}
title={replayPreview.title}
descriptions={replayPreview.descriptions}
subtitle={replayPreview.subtitle}
dialog={replayPreview.dialog}
image={replayPreview.image}
toasts={replayPreview.toasts}
sidebarOpen={replayPreview.sidebarOpen}
playbackSpeed={replayPreview.playbackSpeed}
activeStepLabel={replayPreviewActiveStepLabel}
activeStepNumber={replayPreview.activeStepNumber}
totalSteps={replayPreview.totalSteps}
onPlayPreview={replayPreview.playFromStart}
onStopPreview={replayPreview.stopPreview}
onResetPreview={replayPreview.resetPreview}
onExitPreview={exitReplayPreview}
/>
) : null}
{isReplayPreviewMode && replayPreview.sidebarOpen ? (
<aside
style={{
position: "absolute",
top: 16,
right: 16,
bottom: 16,
width: 420,
maxWidth: "calc(100vw - 2rem)",
zIndex: 16,
}}
>
<PublicWikiSidebar
entity={null}
wiki={replayPreviewActiveWiki}
isLoading={isPreviewWikiLoading}
error={replayPreview.activeWikiId ? previewWikiError : "Chưa có wiki được chọn trong step này."}
onClose={() => {
setPreviewWikiError(null);
replayPreview.closeWikiPanel();
}}
onWikiLinkRequest={handleReplayPreviewWikiLinkRequest}
/> />
) : null} </aside>
</div> ) : null}
) : null} {!isReplayPreviewMode || replayPreview.timelineVisible ? (
<TimelineBar
year={activeTimelineYear}
onYearChange={
isReplayPreviewMode
? replayPreview.setTimelineYear
: handleTimelineYearChange
}
isLoading={false}
disabled={false}
statusText={null}
filterEnabled={activeTimelineFilterEnabled}
onFilterEnabledChange={
isReplayPreviewMode
? replayPreview.setTimelineFilterEnabled
: setTimelineFilterEnabled
}
/>
) : null}
</div>
{!isReplayEditMode && !isReplayPreviewMode ? ( {!isReplayEditMode && !isReplayPreviewMode ? (
<> <>
<ResizeHandle <ResizeHandle
title="Resize right panel" title="Resize right panel"
onDrag={(deltaX) => { onDrag={(deltaX) => {
// dragging handle (between map and right panel): moving right increases right panel width
setRightPanelWidth((prev) => clampNumber(prev - deltaX, 260, 720)); setRightPanelWidth((prev) => clampNumber(prev - deltaX, 260, 720));
}} }}
/> />
+3 -14
View File
@@ -48,33 +48,22 @@ Geotype render hiện được tập trung ở `getAllGeotypeLayers(...)` trong
Các type đang được register: Các type đang được register:
- `defense_line` - `defense_line`
- `attack_route` - `military_route`
- `retreat_route` - `retreat_route`
- `invasion_route`
- `migration_route` - `migration_route`
- `refugee_route`
- `trade_route` - `trade_route`
- `shipping_route`
- `country` - `country`
- `state` - `state`
- `empire`
- `kingdom`
- `faction` - `faction`
- `war`
- `battle` - `battle`
- `civilization`
- `rebellion_zone` - `rebellion_zone`
- `person_deathplace` - `person_event`
- `person_birthplace`
- `person_activity`
- `temple` - `temple`
- `capital` - `capital`
- `city` - `city`
- `fortress` - `fortification`
- `castle`
- `ruin` - `ruin`
- `port` - `port`
- `bridge`
`GEOMETRY_TYPE_OPTIONS` trong `src/uhm/lib/map/geo/geometryTypeOptions.ts` phải khớp với tập geotype này nếu muốn user chọn được từ UI. `GEOMETRY_TYPE_OPTIONS` trong `src/uhm/lib/map/geo/geometryTypeOptions.ts` phải khớp với tập geotype này nếu muốn user chọn được từ UI.
@@ -74,14 +74,7 @@ export function useProjectCommands(options: Options) {
return; return;
} }
const orphanGeometries = findOrphanGeometries(options.editor.mainDraft);
if (orphanGeometries.length > 0) {
const firstOrphan = orphanGeometries[0];
state.setSelectedFeatureIds([firstOrphan.id]);
state.setEntityFormStatus("Geometry này chưa bind entity.");
state.setEntityStatus(formatOrphanGeometryMessage("Commit", orphanGeometries));
return;
}
const geometryChanges = options.editor.buildPayload(); const geometryChanges = options.editor.buildPayload();
state.setIsSaving(true); state.setIsSaving(true);
@@ -221,14 +214,7 @@ export function useProjectCommands(options: Options) {
return; return;
} }
const orphanGeometries = findOrphanGeometries(options.editor.mainDraft);
if (orphanGeometries.length > 0) {
const firstOrphan = orphanGeometries[0];
state.setSelectedFeatureIds([firstOrphan.id]);
state.setEntityFormStatus("Geometry này chưa bind entity.");
state.setEntityStatus(formatOrphanGeometryMessage("Submit", orphanGeometries));
return;
}
state.setIsSubmitting(true); state.setIsSubmitting(true);
state.setEntityStatus(null); state.setEntityStatus(null);
@@ -305,33 +291,7 @@ export function useProjectCommands(options: Options) {
}; };
} }
type OrphanGeometry = {
id: Feature["properties"]["id"];
label: string;
};
function findOrphanGeometries(draft: FeatureCollection): OrphanGeometry[] {
const rows: OrphanGeometry[] = [];
for (const feature of draft.features || []) {
const entityIds = normalizeFeatureEntityIds(feature);
if (entityIds.length > 0) continue;
const id = feature.properties.id;
rows.push({
id,
label: String(id),
});
}
return rows;
}
function formatOrphanGeometryMessage(action: "Commit" | "Submit", rows: OrphanGeometry[]): string {
const sample = rows.slice(0, 8).map((row) => row.label).join(", ");
const more = rows.length > 8 ? `, ... (+${rows.length - 8})` : "";
return `Không thể ${action}: còn ${rows.length} geometry chưa bind entity. Hãy bind entity cho: ${sample}${more}.`;
}
function toEditorSessionSnapshot(snapshot: EditorSnapshot): EditorSnapshot { function toEditorSessionSnapshot(snapshot: EditorSnapshot): EditorSnapshot {
return { return {
+21 -10
View File
@@ -23,6 +23,7 @@ export function initCircle(
// Xóa dữ liệu preview circle trên map. // Xóa dữ liệu preview circle trên map.
const clearPreview = () => { const clearPreview = () => {
if (!map.isStyleLoaded()) return;
(map.getSource("draw-circle-preview") as maplibregl.GeoJSONSource | undefined)?.setData( (map.getSource("draw-circle-preview") as maplibregl.GeoJSONSource | undefined)?.setData(
EMPTY_PREVIEW EMPTY_PREVIEW
); );
@@ -32,7 +33,7 @@ export function initCircle(
const releaseDragPan = () => { const releaseDragPan = () => {
if (!dragPanDisabledByCircle) return; if (!dragPanDisabledByCircle) return;
dragPanDisabledByCircle = false; dragPanDisabledByCircle = false;
if (!map.dragPan.isEnabled()) { if (map.isStyleLoaded() && !map.dragPan.isEnabled()) {
map.dragPan.enable(); map.dragPan.enable();
} }
}; };
@@ -53,6 +54,7 @@ export function initCircle(
return; return;
} }
if (!map.isStyleLoaded()) return;
const ring = buildCircleRing(center, radiusMeters, CIRCLE_SEGMENTS); const ring = buildCircleRing(center, radiusMeters, CIRCLE_SEGMENTS);
(map.getSource("draw-circle-preview") as maplibregl.GeoJSONSource | undefined)?.setData({ (map.getSource("draw-circle-preview") as maplibregl.GeoJSONSource | undefined)?.setData({
type: "FeatureCollection", type: "FeatureCollection",
@@ -91,7 +93,7 @@ export function initCircle(
const onMouseMove = (e: maplibregl.MapMouseEvent) => { const onMouseMove = (e: maplibregl.MapMouseEvent) => {
const canvas = map.getCanvas(); const canvas = map.getCanvas();
if (getMode() !== "add-circle") { if (getMode() !== "add-circle") {
if (canvas.style.cursor === "crosshair") { if (canvas && canvas.style.cursor === "crosshair") {
canvas.style.cursor = ""; canvas.style.cursor = "";
} }
if (isDragging) { if (isDragging) {
@@ -100,7 +102,9 @@ export function initCircle(
return; return;
} }
canvas.style.cursor = "crosshair"; if (canvas) {
canvas.style.cursor = "crosshair";
}
if (!isDragging || !center) return; if (!isDragging || !center) return;
radiusMeters = distanceMeters(center, [e.lngLat.lng, e.lngLat.lat]); radiusMeters = distanceMeters(center, [e.lngLat.lng, e.lngLat.lat]);
@@ -150,13 +154,20 @@ export function initCircle(
document.addEventListener("keydown", onKeyDown); document.addEventListener("keydown", onKeyDown);
const cleanup = () => { const cleanup = () => {
map.off("mousedown", onMouseDown); try {
map.off("mousemove", onMouseMove); map.off("mousedown", onMouseDown);
map.off("mouseup", onMouseUp); map.off("mousemove", onMouseMove);
document.removeEventListener("keydown", onKeyDown); map.off("mouseup", onMouseUp);
resetDrawingState(); document.removeEventListener("keydown", onKeyDown);
if (map.getCanvas().style.cursor === "crosshair") { resetDrawingState();
map.getCanvas().style.cursor = ""; if (map.isStyleLoaded()) {
const canvas = map.getCanvas();
if (canvas && canvas.style.cursor === "crosshair") {
canvas.style.cursor = "";
}
}
} catch {
// ignore
} }
}; };
+14 -6
View File
@@ -12,6 +12,7 @@ export function initDrawing(
let coords: [number, number][] = []; let coords: [number, number][] = [];
const clearPreview = () => { const clearPreview = () => {
if (!map.isStyleLoaded()) return;
(map.getSource("draw-preview") as maplibregl.GeoJSONSource | undefined)?.setData({ (map.getSource("draw-preview") as maplibregl.GeoJSONSource | undefined)?.setData({
type: "FeatureCollection", type: "FeatureCollection",
features: [], features: [],
@@ -39,6 +40,7 @@ export function initDrawing(
function update(c: [number, number][]) { function update(c: [number, number][]) {
const closed = closePolygon(c); const closed = closePolygon(c);
if (!map.isStyleLoaded()) return;
(map.getSource("draw-preview") as maplibregl.GeoJSONSource)?.setData({ (map.getSource("draw-preview") as maplibregl.GeoJSONSource)?.setData({
type: "FeatureCollection", type: "FeatureCollection",
features: [ features: [
@@ -130,12 +132,18 @@ export function initDrawing(
document.addEventListener("keydown", onKeyDown); document.addEventListener("keydown", onKeyDown);
const cleanup = () => { const cleanup = () => {
map.boxZoom.enable(); try {
map.doubleClickZoom.enable(); if (map.isStyleLoaded()) {
map.off("click", onClick); map.boxZoom.enable();
map.off("mousemove", onMove); map.doubleClickZoom.enable();
document.removeEventListener("keydown", onKeyDown); }
cancelDrawing(); map.off("click", onClick);
map.off("mousemove", onMove);
document.removeEventListener("keydown", onKeyDown);
cancelDrawing();
} catch {
// ignore
}
}; };
return { return {
+3 -3
View File
@@ -37,7 +37,7 @@ export function createEditingEngine(options: {
setDeleteVertexMode(false); setDeleteVertexMode(false);
hideContextMenu(); hideContextMenu();
const map = mapRef.current; const map = mapRef.current;
if (!map) return; if (!map || !map.isStyleLoaded()) return;
const empty: GeoJSON.FeatureCollection = { type: "FeatureCollection", features: [] }; const empty: GeoJSON.FeatureCollection = { type: "FeatureCollection", features: [] };
(map.getSource("edit-shape") as maplibregl.GeoJSONSource | undefined)?.setData(empty); (map.getSource("edit-shape") as maplibregl.GeoJSONSource | undefined)?.setData(empty);
(map.getSource("edit-handles") as maplibregl.GeoJSONSource | undefined)?.setData(empty); (map.getSource("edit-handles") as maplibregl.GeoJSONSource | undefined)?.setData(empty);
@@ -47,7 +47,7 @@ export function createEditingEngine(options: {
const updateEditSources = () => { const updateEditSources = () => {
const editing = editingRef.current; const editing = editingRef.current;
const map = mapRef.current; const map = mapRef.current;
if (!editing || !map) return; if (!editing || !map || !map.isStyleLoaded()) return;
let shape: GeoJSON.FeatureCollection<GeoJSON.Polygon>; let shape: GeoJSON.FeatureCollection<GeoJSON.Polygon>;
let handles: GeoJSON.FeatureCollection<GeoJSON.Point>; let handles: GeoJSON.FeatureCollection<GeoJSON.Point>;
@@ -143,7 +143,7 @@ export function createEditingEngine(options: {
const setDeleteVertexMode = (enabled: boolean) => { const setDeleteVertexMode = (enabled: boolean) => {
deleteVertexModeRef.current = enabled; deleteVertexModeRef.current = enabled;
const map = mapRef.current; const map = mapRef.current;
if (!map?.getLayer("edit-handles-circle")) return; if (!map || !map.isStyleLoaded() || !map.getLayer("edit-handles-circle")) return;
map.setPaintProperty("edit-handles-circle", "circle-color", enabled ? "#ef4444" : "#f97316"); map.setPaintProperty("edit-handles-circle", "circle-color", enabled ? "#ef4444" : "#f97316");
map.setPaintProperty("edit-handles-circle", "circle-stroke-color", enabled ? "#7f1d1d" : "#0f172a"); map.setPaintProperty("edit-handles-circle", "circle-stroke-color", enabled ? "#7f1d1d" : "#0f172a");
}; };
+19 -8
View File
@@ -18,6 +18,7 @@ export function initLine(
// Xóa dữ liệu preview line. // Xóa dữ liệu preview line.
const clearPreview = () => { const clearPreview = () => {
if (!map.isStyleLoaded()) return;
(map.getSource("draw-line-preview") as maplibregl.GeoJSONSource | undefined)?.setData( (map.getSource("draw-line-preview") as maplibregl.GeoJSONSource | undefined)?.setData(
EMPTY_PREVIEW EMPTY_PREVIEW
); );
@@ -36,6 +37,7 @@ export function initLine(
return; return;
} }
if (!map.isStyleLoaded()) return;
(map.getSource("draw-line-preview") as maplibregl.GeoJSONSource | undefined)?.setData({ (map.getSource("draw-line-preview") as maplibregl.GeoJSONSource | undefined)?.setData({
type: "FeatureCollection", type: "FeatureCollection",
features: [ features: [
@@ -91,13 +93,15 @@ export function initLine(
if (coords.length) { if (coords.length) {
cancelLine(); cancelLine();
} }
if (canvas.style.cursor === "crosshair") { if (canvas && canvas.style.cursor === "crosshair") {
canvas.style.cursor = ""; canvas.style.cursor = "";
} }
return; return;
} }
canvas.style.cursor = "crosshair"; if (canvas) {
canvas.style.cursor = "crosshair";
}
if (coords.length === 0) return; if (coords.length === 0) return;
const lngLat = e.originalEvent.shiftKey || e.originalEvent.altKey const lngLat = e.originalEvent.shiftKey || e.originalEvent.altKey
@@ -133,12 +137,19 @@ export function initLine(
document.addEventListener("keydown", onKeyDown); document.addEventListener("keydown", onKeyDown);
const cleanup = () => { const cleanup = () => {
map.off("click", onClick); try {
map.off("mousemove", onMove); map.off("click", onClick);
document.removeEventListener("keydown", onKeyDown); map.off("mousemove", onMove);
cancelLine(); document.removeEventListener("keydown", onKeyDown);
if (map.getCanvas().style.cursor === "crosshair") { cancelLine();
map.getCanvas().style.cursor = ""; if (map.isStyleLoaded()) {
const canvas = map.getCanvas();
if (canvas && canvas.style.cursor === "crosshair") {
canvas.style.cursor = "";
}
}
} catch {
// ignore
} }
}; };
+19 -8
View File
@@ -18,6 +18,7 @@ export function initPath(
// Xóa dữ liệu preview path. // Xóa dữ liệu preview path.
const clearPreview = () => { const clearPreview = () => {
if (!map.isStyleLoaded()) return;
(map.getSource("draw-path-preview") as maplibregl.GeoJSONSource | undefined)?.setData( (map.getSource("draw-path-preview") as maplibregl.GeoJSONSource | undefined)?.setData(
EMPTY_PREVIEW EMPTY_PREVIEW
); );
@@ -30,6 +31,7 @@ export function initPath(
return; return;
} }
if (!map.isStyleLoaded()) return;
(map.getSource("draw-path-preview") as maplibregl.GeoJSONSource | undefined)?.setData({ (map.getSource("draw-path-preview") as maplibregl.GeoJSONSource | undefined)?.setData({
type: "FeatureCollection", type: "FeatureCollection",
features: [ features: [
@@ -92,13 +94,15 @@ export function initPath(
if (coords.length) { if (coords.length) {
cancelPath(); cancelPath();
} }
if (canvas.style.cursor === "crosshair") { if (canvas && canvas.style.cursor === "crosshair") {
canvas.style.cursor = ""; canvas.style.cursor = "";
} }
return; return;
} }
canvas.style.cursor = "crosshair"; if (canvas) {
canvas.style.cursor = "crosshair";
}
if (coords.length === 0) return; if (coords.length === 0) return;
const lngLat = e.originalEvent.shiftKey || e.originalEvent.altKey const lngLat = e.originalEvent.shiftKey || e.originalEvent.altKey
@@ -134,12 +138,19 @@ export function initPath(
document.addEventListener("keydown", onKeyDown); document.addEventListener("keydown", onKeyDown);
const cleanup = () => { const cleanup = () => {
map.off("click", onClick); try {
map.off("mousemove", onMove); map.off("click", onClick);
document.removeEventListener("keydown", onKeyDown); map.off("mousemove", onMove);
cancelPath(); document.removeEventListener("keydown", onKeyDown);
if (map.getCanvas().style.cursor === "crosshair") { cancelPath();
map.getCanvas().style.cursor = ""; if (map.isStyleLoaded()) {
const canvas = map.getCanvas();
if (canvas && canvas.style.cursor === "crosshair") {
canvas.style.cursor = "";
}
}
} catch {
// ignore
} }
}; };
+11 -4
View File
@@ -41,10 +41,17 @@ export function initPoint(
map.on("mousemove", onMove); map.on("mousemove", onMove);
return () => { return () => {
map.off("click", onClick); try {
map.off("mousemove", onMove); map.off("click", onClick);
if (map.getCanvas().style.cursor === "crosshair") { map.off("mousemove", onMove);
map.getCanvas().style.cursor = ""; if (map.isStyleLoaded()) {
const canvas = map.getCanvas();
if (canvas && canvas.style.cursor === "crosshair") {
canvas.style.cursor = "";
}
}
} catch {
// ignore
} }
}; };
} }
+13 -6
View File
@@ -150,6 +150,7 @@ export function initSelect(
} }
function setSelectionStateForId(id: string | number, selected: boolean) { function setSelectionStateForId(id: string | number, selected: boolean) {
if (!map.isStyleLoaded()) return;
for (const source of FEATURE_STATE_SOURCES) { for (const source of FEATURE_STATE_SOURCES) {
if (!map.getSource(source)) continue; if (!map.getSource(source)) continue;
map.setFeatureState({ source, id }, { selected }); map.setFeatureState({ source, id }, { selected });
@@ -194,13 +195,19 @@ export function initSelect(
} }
const cleanup = () => { const cleanup = () => {
map.off("click", onClick); try {
map.off("mousemove", onMove); map.off("click", onClick);
if (hasContextActions) { map.off("mousemove", onMove);
map.off("contextmenu", onRightClick); if (hasContextActions) {
map.off("contextmenu", onRightClick);
}
if (map.isStyleLoaded()) {
clearSelection(false);
}
hideContextMenu();
} catch {
// ignore
} }
clearSelection(false);
hideContextMenu();
}; };
return { return {
+10 -15
View File
@@ -1,33 +1,28 @@
[ [
{ "type_key": "defense_line", "geo_type_code": 1 }, { "type_key": "defense_line", "geo_type_code": 1 },
{ "type_key": "attack_route", "geo_type_code": 2 }, { "type_key": "military_route", "geo_type_code": 2 },
{ "type_key": "retreat_route", "geo_type_code": 3 }, { "type_key": "retreat_route", "geo_type_code": 3 },
{ "type_key": "invasion_route", "geo_type_code": 4 },
{ "type_key": "migration_route", "geo_type_code": 5 }, { "type_key": "migration_route", "geo_type_code": 5 },
{ "type_key": "refugee_route", "geo_type_code": 6 },
{ "type_key": "trade_route", "geo_type_code": 7 }, { "type_key": "trade_route", "geo_type_code": 7 },
{ "type_key": "shipping_route", "geo_type_code": 8 },
{ "type_key": "country", "geo_type_code": 9, "fixed": true }, { "type_key": "country", "geo_type_code": 9, "fixed": true },
{ "type_key": "state", "geo_type_code": 10 }, { "type_key": "state", "geo_type_code": 10 },
{ "type_key": "empire", "geo_type_code": 11 },
{ "type_key": "kingdom", "geo_type_code": 12 },
{ "type_key": "faction", "geo_type_code": 28 }, { "type_key": "faction", "geo_type_code": 28 },
{ "type_key": "war", "geo_type_code": 13 },
{ "type_key": "battle", "geo_type_code": 14 }, { "type_key": "battle", "geo_type_code": 14 },
{ "type_key": "civilization", "geo_type_code": 15 },
{ "type_key": "rebellion_zone", "geo_type_code": 16 }, { "type_key": "rebellion_zone", "geo_type_code": 16 },
{ "type_key": "person_deathplace", "geo_type_code": 17 }, { "type_key": "person_event", "geo_type_code": 17 },
{ "type_key": "person_birthplace", "geo_type_code": 18 }, { "type_key": "person_event", "geo_type_code": 18 },
{ "type_key": "person_activity", "geo_type_code": 19 }, { "type_key": "person_event", "geo_type_code": 19 },
{ "type_key": "temple", "geo_type_code": 20 }, { "type_key": "temple", "geo_type_code": 20 },
{ "type_key": "capital", "geo_type_code": 21 }, { "type_key": "capital", "geo_type_code": 21 },
{ "type_key": "city", "geo_type_code": 22 }, { "type_key": "city", "geo_type_code": 22 },
{ "type_key": "fortress", "geo_type_code": 23 },
{ "type_key": "castle", "geo_type_code": 24 }, { "type_key": "fortification", "geo_type_code": 23 },
{ "type_key": "fortification", "geo_type_code": 24 },
{ "type_key": "ruin", "geo_type_code": 25 }, { "type_key": "ruin", "geo_type_code": 25 },
{ "type_key": "port", "geo_type_code": 26 }, { "type_key": "port", "geo_type_code": 26 }
{ "type_key": "bridge", "geo_type_code": 27 }
] ]
+18
View File
@@ -40,6 +40,15 @@ export function geoTypeCodeToTypeKey(code: number | null | undefined): string |
return KEY_BY_CODE.get(Math.trunc(code)) ?? null; return KEY_BY_CODE.get(Math.trunc(code)) ?? null;
} }
const DEPRECATED_MAPPING: Record<string, string> = {
attack_route: "military_route",
person_birthplace: "person_event",
person_deathplace: "person_event",
person_activity: "person_event",
fortress: "fortification",
castle: "fortification",
};
export function normalizeGeoTypeKey(value: unknown): string | null { export function normalizeGeoTypeKey(value: unknown): string | null {
if (typeof value === "number") { if (typeof value === "number") {
return geoTypeCodeToTypeKey(value); return geoTypeCodeToTypeKey(value);
@@ -54,5 +63,14 @@ export function normalizeGeoTypeKey(value: unknown): string | null {
return geoTypeCodeToTypeKey(Number(normalized)); return geoTypeCodeToTypeKey(Number(normalized));
} }
if (normalized in DEPRECATED_MAPPING) {
return DEPRECATED_MAPPING[normalized];
}
const code = CODE_BY_KEY.get(normalized);
if (code !== undefined) {
return KEY_BY_CODE.get(code) ?? normalized;
}
return normalized; return normalized;
} }
+3 -15
View File
@@ -62,37 +62,25 @@ const RAW_GEOMETRY_TYPE_OPTIONS: Array<{
geometryPreset: GeometryPreset; geometryPreset: GeometryPreset;
}> = [ }> = [
{ value: "defense_line", label: "Defense Line", groupId: "line", geometryPreset: "line" }, { value: "defense_line", label: "Defense Line", groupId: "line", geometryPreset: "line" },
{ value: "military_route", label: "Military Route", groupId: "line", geometryPreset: "line" },
{ value: "attack_route", label: "Attack Route", groupId: "line", geometryPreset: "line" },
{ value: "retreat_route", label: "Retreat Route", groupId: "line", geometryPreset: "line" }, { value: "retreat_route", label: "Retreat Route", groupId: "line", geometryPreset: "line" },
{ value: "invasion_route", label: "Invasion Route", groupId: "line", geometryPreset: "line" },
{ value: "migration_route", label: "Migration Route", groupId: "line", geometryPreset: "line" }, { value: "migration_route", label: "Migration Route", groupId: "line", geometryPreset: "line" },
{ value: "refugee_route", label: "Refugee Route", groupId: "line", geometryPreset: "line" },
{ value: "trade_route", label: "Trade Route", groupId: "line", geometryPreset: "line" }, { value: "trade_route", label: "Trade Route", groupId: "line", geometryPreset: "line" },
{ value: "shipping_route", label: "Shipping Route", groupId: "line", geometryPreset: "line" },
{ value: "country", label: "Country", groupId: "polygon", geometryPreset: "polygon" }, { value: "country", label: "Country", groupId: "polygon", geometryPreset: "polygon" },
{ value: "state", label: "State", groupId: "polygon", geometryPreset: "polygon" }, { value: "state", label: "State", groupId: "polygon", geometryPreset: "polygon" },
{ value: "empire", label: "Empire", groupId: "polygon", geometryPreset: "polygon" },
{ value: "kingdom", label: "Kingdom", groupId: "polygon", geometryPreset: "polygon" },
{ value: "faction", label: "Faction", groupId: "polygon", geometryPreset: "polygon" }, { value: "faction", label: "Faction", groupId: "polygon", geometryPreset: "polygon" },
{ value: "war", label: "War", groupId: "circle", geometryPreset: "circle-area" },
{ value: "battle", label: "Battle", groupId: "circle", geometryPreset: "circle-area" }, { value: "battle", label: "Battle", groupId: "circle", geometryPreset: "circle-area" },
{ value: "civilization", label: "Civilization", groupId: "circle", geometryPreset: "circle-area" },
{ value: "rebellion_zone", label: "Rebellion Zone", groupId: "circle", geometryPreset: "circle-area" }, { value: "rebellion_zone", label: "Rebellion Zone", groupId: "circle", geometryPreset: "circle-area" },
{ value: "person_deathplace", label: "Person Deathplace", groupId: "point", geometryPreset: "point" }, { value: "person_event", label: "Person Event", groupId: "point", geometryPreset: "point" },
{ value: "person_birthplace", label: "Person Birthplace", groupId: "point", geometryPreset: "point" },
{ value: "person_activity", label: "Person Activity", groupId: "point", geometryPreset: "point" },
{ value: "temple", label: "Temple", groupId: "point", geometryPreset: "point" }, { value: "temple", label: "Temple", groupId: "point", geometryPreset: "point" },
{ value: "capital", label: "Capital", groupId: "point", geometryPreset: "point" }, { value: "capital", label: "Capital", groupId: "point", geometryPreset: "point" },
{ value: "city", label: "City", groupId: "point", geometryPreset: "point" }, { value: "city", label: "City", groupId: "point", geometryPreset: "point" },
{ value: "fortress", label: "Fortress", groupId: "point", geometryPreset: "point" }, { value: "fortification", label: "Fortification", groupId: "point", geometryPreset: "point" },
{ value: "castle", label: "Castle", groupId: "point", geometryPreset: "point" },
{ value: "ruin", label: "Ruin", groupId: "point", geometryPreset: "point" }, { value: "ruin", label: "Ruin", groupId: "point", geometryPreset: "point" },
{ value: "port", label: "Port", groupId: "point", geometryPreset: "point" }, { value: "port", label: "Port", groupId: "point", geometryPreset: "point" },
{ value: "bridge", label: "Bridge", groupId: "point", geometryPreset: "point" },
]; ];
export const GEOMETRY_TYPE_OPTIONS: GeometryTypeOption[] = RAW_GEOMETRY_TYPE_OPTIONS.map((item) => ({ export const GEOMETRY_TYPE_OPTIONS: GeometryTypeOption[] = RAW_GEOMETRY_TYPE_OPTIONS.map((item) => ({
+7 -29
View File
@@ -3,33 +3,22 @@ export const TYPE_MATCH_EXPR: maplibregl.ExpressionSpecification = ["coalesce",
export { ensurePointGeotypeIcons } from "./shared/pointStyle"; export { ensurePointGeotypeIcons } from "./shared/pointStyle";
import { getDefenseLineLayers } from "./geotypes/defense_line"; import { getDefenseLineLayers } from "./geotypes/defense_line";
import { getAttackRouteLayers } from "./geotypes/attack_route"; import { getMilitaryRouteLayers } from "./geotypes/military_route";
import { getRetreatRouteLayers } from "./geotypes/retreat_route"; import { getRetreatRouteLayers } from "./geotypes/retreat_route";
import { getInvasionRouteLayers } from "./geotypes/invasion_route";
import { getMigrationRouteLayers } from "./geotypes/migration_route"; import { getMigrationRouteLayers } from "./geotypes/migration_route";
import { getRefugeeRouteLayers } from "./geotypes/refugee_route";
import { getTradeRouteLayers } from "./geotypes/trade_route"; import { getTradeRouteLayers } from "./geotypes/trade_route";
import { getShippingRouteLayers } from "./geotypes/shipping_route";
import { getCountryLayers } from "./geotypes/country"; import { getCountryLayers } from "./geotypes/country";
import { getStateLayers } from "./geotypes/state"; import { getStateLayers } from "./geotypes/state";
import { getEmpireLayers } from "./geotypes/empire";
import { getKingdomLayers } from "./geotypes/kingdom";
import { getFactionLayers } from "./geotypes/faction"; import { getFactionLayers } from "./geotypes/faction";
import { getWarLayers } from "./geotypes/war";
import { getBattleLayers } from "./geotypes/battle"; import { getBattleLayers } from "./geotypes/battle";
import { getCivilizationLayers } from "./geotypes/civilization";
import { getRebellionZoneLayers } from "./geotypes/rebellion_zone"; import { getRebellionZoneLayers } from "./geotypes/rebellion_zone";
import { getPersonDeathplaceLayers } from "./geotypes/person_deathplace"; import { getPersonEventLayers } from "./geotypes/person_event";
import { getPersonBirthplaceLayers } from "./geotypes/person_birthplace";
import { getPersonActivityLayers } from "./geotypes/person_activity";
import { getTempleLayers } from "./geotypes/temple"; import { getTempleLayers } from "./geotypes/temple";
import { getCapitalLayers } from "./geotypes/capital"; import { getCapitalLayers } from "./geotypes/capital";
import { getCityLayers } from "./geotypes/city"; import { getCityLayers } from "./geotypes/city";
import { getFortressLayers } from "./geotypes/fortress"; import { getFortificationLayers } from "./geotypes/fortification";
import { getCastleLayers } from "./geotypes/castle";
import { getRuinLayers } from "./geotypes/ruin"; import { getRuinLayers } from "./geotypes/ruin";
import { getPortLayers } from "./geotypes/port"; import { getPortLayers } from "./geotypes/port";
import { getBridgeLayers } from "./geotypes/bridge";
import { getLineLabelLayers } from "./shared/lineLabels"; import { getLineLabelLayers } from "./shared/lineLabels";
import { getPolygonLabelLayers } from "./shared/polygonLabels"; import { getPolygonLabelLayers } from "./shared/polygonLabels";
@@ -39,32 +28,21 @@ export function getAllGeotypeLayers(sourceId: string, pathArrowSourceId?: string
return [ return [
...getCountryLayers(sourceId, pathArrowSourceId, pointSourceId), ...getCountryLayers(sourceId, pathArrowSourceId, pointSourceId),
...getStateLayers(sourceId, pathArrowSourceId, pointSourceId), ...getStateLayers(sourceId, pathArrowSourceId, pointSourceId),
...getEmpireLayers(sourceId, pathArrowSourceId, pointSourceId),
...getKingdomLayers(sourceId, pathArrowSourceId, pointSourceId),
...getFactionLayers(sourceId, pathArrowSourceId, pointSourceId), ...getFactionLayers(sourceId, pathArrowSourceId, pointSourceId),
...getWarLayers(sourceId, pathArrowSourceId, pointSourceId),
...getBattleLayers(sourceId, pathArrowSourceId, pointSourceId), ...getBattleLayers(sourceId, pathArrowSourceId, pointSourceId),
...getCivilizationLayers(sourceId, pathArrowSourceId, pointSourceId),
...getRebellionZoneLayers(sourceId, pathArrowSourceId, pointSourceId), ...getRebellionZoneLayers(sourceId, pathArrowSourceId, pointSourceId),
...getDefenseLineLayers(sourceId, pathArrowSourceId, pointSourceId), ...getDefenseLineLayers(sourceId, pathArrowSourceId, pointSourceId),
...getAttackRouteLayers(sourceId, pathArrowSourceId, pointSourceId), ...getMilitaryRouteLayers(sourceId, pathArrowSourceId, pointSourceId),
...getRetreatRouteLayers(sourceId, pathArrowSourceId, pointSourceId), ...getRetreatRouteLayers(sourceId, pathArrowSourceId, pointSourceId),
...getInvasionRouteLayers(sourceId, pathArrowSourceId, pointSourceId),
...getMigrationRouteLayers(sourceId, pathArrowSourceId, pointSourceId), ...getMigrationRouteLayers(sourceId, pathArrowSourceId, pointSourceId),
...getRefugeeRouteLayers(sourceId, pathArrowSourceId, pointSourceId),
...getTradeRouteLayers(sourceId, pathArrowSourceId, pointSourceId), ...getTradeRouteLayers(sourceId, pathArrowSourceId, pointSourceId),
...getShippingRouteLayers(sourceId, pathArrowSourceId, pointSourceId), ...getPersonEventLayers(sourceId, pathArrowSourceId, pointSourceId),
...getPersonDeathplaceLayers(sourceId, pathArrowSourceId, pointSourceId),
...getPersonBirthplaceLayers(sourceId, pathArrowSourceId, pointSourceId),
...getPersonActivityLayers(sourceId, pathArrowSourceId, pointSourceId),
...getTempleLayers(sourceId, pathArrowSourceId, pointSourceId), ...getTempleLayers(sourceId, pathArrowSourceId, pointSourceId),
...getCapitalLayers(sourceId, pathArrowSourceId, pointSourceId), ...getCapitalLayers(sourceId, pathArrowSourceId, pointSourceId),
...getCityLayers(sourceId, pathArrowSourceId, pointSourceId), ...getCityLayers(sourceId, pathArrowSourceId, pointSourceId),
...getFortressLayers(sourceId, pathArrowSourceId, pointSourceId), ...getFortificationLayers(sourceId, pathArrowSourceId, pointSourceId),
...getCastleLayers(sourceId, pathArrowSourceId, pointSourceId),
...getRuinLayers(sourceId, pathArrowSourceId, pointSourceId), ...getRuinLayers(sourceId, pathArrowSourceId, pointSourceId),
...getPortLayers(sourceId, pathArrowSourceId, pointSourceId), ...getPortLayers(sourceId, pathArrowSourceId, pointSourceId)
...getBridgeLayers(sourceId, pathArrowSourceId, pointSourceId)
]; ];
} }
@@ -1,8 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getBridgeLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId;
void pathArrowSourceId;
return buildPointGeotypeLayers("bridge", pointSourceId!);
}
@@ -1,8 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getCastleLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId;
void pathArrowSourceId;
return buildPointGeotypeLayers("castle", pointSourceId!);
}
@@ -1,13 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getCivilizationLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId;
void pointSourceId;
return buildPolygonGeotypeLayers(sourceId, {
typeId: "civilization",
fillColor: "#14b8a6",
strokeColor: "#134e4a",
fillOpacity: 0.34,
});
}
-14
View File
@@ -1,14 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getEmpireLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId;
void pointSourceId;
return buildPolygonGeotypeLayers(sourceId, {
typeId: "empire",
fillColor: "#f59e0b",
strokeColor: "#92400e",
fillOpacity: 0.36,
strokeWidth: { z1: 1.8, z4: 2.6, z6: 3.4 },
});
}
@@ -0,0 +1,8 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getFortificationLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId;
void pathArrowSourceId;
return buildPointGeotypeLayers("fortification", pointSourceId!);
}
@@ -1,8 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getFortressLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId;
void pathArrowSourceId;
return buildPointGeotypeLayers("fortress", pointSourceId!);
}
@@ -1,13 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getInvasionRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId;
return buildLineGeotypeLayers(sourceId, pathArrowSourceId, {
typeId: "invasion_route",
color: "#be123c",
strokeColor: "#4c0519",
width: { z1: 2.8, z4: 4.1, z6: 5.4 },
arrowOpacity: 0.9,
});
}
@@ -1,13 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getKingdomLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId;
void pointSourceId;
return buildPolygonGeotypeLayers(sourceId, {
typeId: "kingdom",
fillColor: "#8b5cf6",
strokeColor: "#6d28d9",
fillOpacity: 0.34,
});
}
@@ -1,10 +1,10 @@
import { LayerSpecification } from "maplibre-gl"; import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "../shared/styleBuilders"; import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getAttackRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] { export function getMilitaryRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId; void pointSourceId;
return buildLineGeotypeLayers(sourceId, pathArrowSourceId, { return buildLineGeotypeLayers(sourceId, pathArrowSourceId, {
typeId: "attack_route", typeId: "military_route",
color: "#ef4444", color: "#ef4444",
strokeColor: "#7f1d1d", strokeColor: "#7f1d1d",
width: { z1: 2.6, z4: 3.8, z6: 5 }, width: { z1: 2.6, z4: 3.8, z6: 5 },
@@ -1,8 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getPersonActivityLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId;
void pathArrowSourceId;
return buildPointGeotypeLayers("person_activity", pointSourceId!);
}
@@ -1,8 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getPersonBirthplaceLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId;
void pathArrowSourceId;
return buildPointGeotypeLayers("person_birthplace", pointSourceId!);
}
@@ -1,8 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getPersonDeathplaceLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId;
void pathArrowSourceId;
return buildPointGeotypeLayers("person_deathplace", pointSourceId!);
}
@@ -0,0 +1,8 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPointGeotypeLayers } from "../shared/pointStyle";
export function getPersonEventLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void sourceId;
void pathArrowSourceId;
return buildPointGeotypeLayers("person_event", pointSourceId!);
}
@@ -1,14 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getRefugeeRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId;
return buildLineGeotypeLayers(sourceId, pathArrowSourceId, {
typeId: "refugee_route",
color: "#f97316",
strokeColor: "#9a3412",
dasharray: [1, 2],
opacity: 0.84,
arrowOpacity: 0.72,
});
}
@@ -1,14 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildLineGeotypeLayers } from "../shared/styleBuilders";
export function getShippingRouteLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pointSourceId;
return buildLineGeotypeLayers(sourceId, pathArrowSourceId, {
typeId: "shipping_route",
color: "#0ea5e9",
strokeColor: "#075985",
width: { z1: 2.4, z4: 3.5, z6: 4.7 },
dasharray: [7, 4],
arrowOpacity: 0.8,
});
}
-14
View File
@@ -1,14 +0,0 @@
import { LayerSpecification } from "maplibre-gl";
import { buildPolygonGeotypeLayers } from "../shared/styleBuilders";
export function getWarLayers(sourceId: string, pathArrowSourceId?: string, pointSourceId?: string): LayerSpecification[] {
void pathArrowSourceId;
void pointSourceId;
return buildPolygonGeotypeLayers(sourceId, {
typeId: "war",
fillColor: "#dc2626",
strokeColor: "#7f1d1d",
fillOpacity: 0.26,
dasharray: [5, 2],
});
}
+10 -112
View File
@@ -2,30 +2,23 @@ import maplibregl, { LayerSpecification } from "maplibre-gl";
import { MAP_EMPHASIS_TEXT_FONT_STACK } from "./textFonts"; import { MAP_EMPHASIS_TEXT_FONT_STACK } from "./textFonts";
export const POINT_GEOTYPE_IDS = [ export const POINT_GEOTYPE_IDS = [
"person_birthplace", "person_event",
"person_deathplace",
"person_activity",
"temple", "temple",
"capital", "capital",
"city", "city",
"fortress", "fortification",
"castle",
"ruin", "ruin",
"port", "port",
"bridge",
] as const; ] as const;
export type PointGeotypeId = (typeof POINT_GEOTYPE_IDS)[number]; export type PointGeotypeId = (typeof POINT_GEOTYPE_IDS)[number];
export const POINT_GEOTYPE_ICON_PATHS: Partial<Record<PointGeotypeId, string>> = { export const POINT_GEOTYPE_ICON_PATHS: Partial<Record<PointGeotypeId, string>> = {
person_birthplace: "/images/mapIcon/point/house.png", person_event: "/images/mapIcon/point/flag.png",
person_deathplace: "/images/mapIcon/point/tombstone.png",
person_activity: "/images/mapIcon/point/flag.png",
temple: "/images/mapIcon/point/temple.png", temple: "/images/mapIcon/point/temple.png",
capital: "/images/mapIcon/point/capital.png", capital: "/images/mapIcon/point/capital.png",
city: "/images/mapIcon/point/city.png", city: "/images/mapIcon/point/city.png",
fortress: "/images/mapIcon/point/fortress.png", fortification: "/images/mapIcon/point/castle.png",
castle: "/images/mapIcon/point/castle.png",
ruin: "/images/mapIcon/point/ruin.png", ruin: "/images/mapIcon/point/ruin.png",
}; };
@@ -57,21 +50,7 @@ const POINT_GEOMETRY_FILTER: maplibregl.ExpressionSpecification = [
]; ];
const POINT_STYLE_CONFIG: Record<PointGeotypeId, PointStyleConfig> = { const POINT_STYLE_CONFIG: Record<PointGeotypeId, PointStyleConfig> = {
person_birthplace: { person_event: {
fill: "#22c55e",
rim: "#166534",
iconScale: 1,
haloRadius: 15,
drawGlyph: drawHouseGlyph,
},
person_deathplace: {
fill: "#b91c1c",
rim: "#450a0a",
iconScale: 1,
haloRadius: 15,
drawGlyph: drawMemorialGlyph,
},
person_activity: {
fill: "#f97316", fill: "#f97316",
rim: "#9a3412", rim: "#9a3412",
iconScale: 0.98, iconScale: 0.98,
@@ -99,14 +78,7 @@ const POINT_STYLE_CONFIG: Record<PointGeotypeId, PointStyleConfig> = {
haloRadius: 15, haloRadius: 15,
drawGlyph: drawCityGlyph, drawGlyph: drawCityGlyph,
}, },
fortress: { fortification: {
fill: "#64748b",
rim: "#334155",
iconScale: 1.04,
haloRadius: 16,
drawGlyph: drawShieldGlyph,
},
castle: {
fill: "#7c3aed", fill: "#7c3aed",
rim: "#4c1d95", rim: "#4c1d95",
iconScale: 1.04, iconScale: 1.04,
@@ -127,13 +99,6 @@ const POINT_STYLE_CONFIG: Record<PointGeotypeId, PointStyleConfig> = {
haloRadius: 15, haloRadius: 15,
drawGlyph: drawAnchorGlyph, drawGlyph: drawAnchorGlyph,
}, },
bridge: {
fill: "#b45309",
rim: "#7c2d12",
iconScale: 1,
haloRadius: 14,
drawGlyph: drawBridgeGlyph,
},
}; };
export function buildPointGeotypeLayers( export function buildPointGeotypeLayers(
@@ -320,53 +285,9 @@ function drawGlyphWithOutline(
ctx.restore(); ctx.restore();
} }
function drawHouseGlyph(ctx: CanvasRenderingContext2D) {
const img = preloadedImages["person_birthplace"];
if (img && loadedImageKeys.has("person_birthplace")) {
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
} else {
ctx.lineWidth = 3.5;
ctx.beginPath();
ctx.moveTo(22, 34);
ctx.lineTo(32, 24);
ctx.lineTo(42, 34);
ctx.stroke();
ctx.beginPath();
ctx.rect(25.5, 34, 13, 9);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(32, 43);
ctx.lineTo(32, 36.5);
ctx.stroke();
}
}
function drawMemorialGlyph(ctx: CanvasRenderingContext2D) {
const img = preloadedImages["person_deathplace"];
if (img && loadedImageKeys.has("person_deathplace")) {
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
} else {
ctx.lineWidth = 3.6;
ctx.beginPath();
ctx.moveTo(32, 22);
ctx.lineTo(32, 43);
ctx.moveTo(25, 28.5);
ctx.lineTo(39, 28.5);
ctx.stroke();
ctx.lineWidth = 2.4;
ctx.beginPath();
ctx.moveTo(24, 45);
ctx.lineTo(40, 45);
ctx.stroke();
}
}
function drawFlagGlyph(ctx: CanvasRenderingContext2D) { function drawFlagGlyph(ctx: CanvasRenderingContext2D) {
const img = preloadedImages["person_activity"]; const img = preloadedImages["person_event"];
if (img && loadedImageKeys.has("person_activity")) { if (img && loadedImageKeys.has("person_event")) {
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE); ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
} else { } else {
ctx.lineWidth = 3.2; ctx.lineWidth = 3.2;
@@ -462,32 +383,9 @@ function drawCityGlyph(ctx: CanvasRenderingContext2D) {
} }
} }
function drawShieldGlyph(ctx: CanvasRenderingContext2D) {
const img = preloadedImages["fortress"];
if (img && loadedImageKeys.has("fortress")) {
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
} else {
ctx.lineWidth = 3.2;
ctx.beginPath();
ctx.moveTo(32, 22.5);
ctx.lineTo(41, 26.5);
ctx.lineTo(39, 37.5);
ctx.lineTo(32, 43);
ctx.lineTo(25, 37.5);
ctx.lineTo(23, 26.5);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(32, 25);
ctx.lineTo(32, 39);
ctx.stroke();
}
}
function drawCastleGlyph(ctx: CanvasRenderingContext2D) { function drawCastleGlyph(ctx: CanvasRenderingContext2D) {
const img = preloadedImages["castle"]; const img = preloadedImages["fortification"];
if (img && loadedImageKeys.has("castle")) { if (img && loadedImageKeys.has("fortification")) {
ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE); ctx.drawImage(img, 0, 0, ICON_CANVAS_SIZE, ICON_CANVAS_SIZE);
} else { } else {
ctx.lineWidth = 3; ctx.lineWidth = 3;
+2 -18
View File
@@ -27,50 +27,34 @@ export const COUNTRY_FILL_COLOR_EXPRESSION: maplibregl.ExpressionSpecification =
export const POLYGON_FILL_BY_TYPE: Record<string, string> = { export const POLYGON_FILL_BY_TYPE: Record<string, string> = {
country: "#2563eb", country: "#2563eb",
state: "#0ea5e9", state: "#0ea5e9",
empire: "#f59e0b",
kingdom: "#d97706",
war: "#dc2626",
battle: "#f43f5e", battle: "#f43f5e",
civilization: "#14b8a6",
rebellion_zone: "#7c3aed", rebellion_zone: "#7c3aed",
}; };
export const POLYGON_STROKE_BY_TYPE: Record<string, string> = { export const POLYGON_STROKE_BY_TYPE: Record<string, string> = {
country: "#1e3a8a", country: "#1e3a8a",
state: "#0c4a6e", state: "#0c4a6e",
empire: "#7c2d12",
kingdom: "#9a3412",
war: "#7f1d1d",
battle: "#9f1239", battle: "#9f1239",
civilization: "#134e4a",
rebellion_zone: "#4c1d95", rebellion_zone: "#4c1d95",
}; };
export const POLYGON_OPACITY_BY_TYPE: Record<string, number> = { export const POLYGON_OPACITY_BY_TYPE: Record<string, number> = {
war: 0.3,
battle: 0.34, battle: 0.34,
civilization: 0.38,
rebellion_zone: 0.32, rebellion_zone: 0.32,
}; };
export const LINE_COLOR_BY_TYPE: Record<string, string> = { export const LINE_COLOR_BY_TYPE: Record<string, string> = {
defense_line: "#f97316", defense_line: "#f97316",
attack_route: "#ef4444", military_route: "#ef4444",
retreat_route: "#94a3b8", retreat_route: "#94a3b8",
invasion_route: "#b91c1c",
migration_route: "#0ea5e9", migration_route: "#0ea5e9",
refugee_route: "#06b6d4",
trade_route: "#eab308", trade_route: "#eab308",
shipping_route: "#2563eb",
}; };
export const PATH_RENDER_BY_TYPE: Record<string, boolean> = { export const PATH_RENDER_BY_TYPE: Record<string, boolean> = {
attack_route: true, military_route: true,
retreat_route: true, retreat_route: true,
invasion_route: true,
migration_route: true, migration_route: true,
refugee_route: true,
trade_route: true, trade_route: true,
shipping_route: true,
}; };