refactor
This commit is contained in:
@@ -627,6 +627,8 @@ function formatUndoLabel(action: UndoAction) {
|
||||
return `Xóa #${action.feature.properties.id}`;
|
||||
case "update":
|
||||
return `Chỉnh sửa #${action.id}`;
|
||||
case "properties":
|
||||
return `Cập nhật thuộc tính #${action.id}`;
|
||||
default:
|
||||
return "Tác vụ";
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ import maplibregl from "maplibre-gl";
|
||||
import "maplibre-gl/dist/maplibre-gl.css";
|
||||
|
||||
import { getRasterTileTemplateUrl, getVectorTileTemplateUrl } from "@/api/tiles";
|
||||
import { initDrawing } from "@/lib/drawingEngine";
|
||||
import { initSelect } from "@/lib/selectingEngine";
|
||||
import { initPoint } from "@/lib/pointEngine";
|
||||
import { initLine } from "@/lib/lineEngine";
|
||||
import { initPath } from "@/lib/pathEngine";
|
||||
import { initCircle } from "@/lib/circleEngine";
|
||||
import { createEditingEngine } from "@/lib/editingEngine";
|
||||
import { initDrawing } from "@/lib/engine/drawingEngine";
|
||||
import { initSelect } from "@/lib/engine/selectingEngine";
|
||||
import { initPoint } from "@/lib/engine/pointEngine";
|
||||
import { initLine } from "@/lib/engine/lineEngine";
|
||||
import { initPath } from "@/lib/engine/pathEngine";
|
||||
import { initCircle } from "@/lib/engine/circleEngine";
|
||||
import { createEditingEngine } from "@/lib/engine/editingEngine";
|
||||
import { Feature, FeatureCollection, Geometry } from "@/lib/useEditorState";
|
||||
import { BACKGROUND_LAYER_OPTIONS, BackgroundLayerVisibility } from "@/lib/backgroundLayers";
|
||||
|
||||
@@ -31,6 +31,12 @@ type MapProps = {
|
||||
fitBoundsKey?: string | number | null;
|
||||
};
|
||||
|
||||
type EngineBinding = {
|
||||
cleanup: () => void;
|
||||
cancel?: () => void;
|
||||
clearSelection?: () => void;
|
||||
};
|
||||
|
||||
const DEFAULT_POINT_ICON_ID = "point-icon-default";
|
||||
const POINT_ICON_URL = "/point.png";
|
||||
const PATH_ARROW_ICON_ID = "path-arrow-icon";
|
||||
@@ -148,14 +154,28 @@ export default function Map({
|
||||
const editingEngineRef = useRef<ReturnType<typeof createEditingEngine> | null>(null);
|
||||
const fitBoundsAppliedRef = useRef(false);
|
||||
const mapCleanupFnsRef = useRef<Array<() => void>>([]);
|
||||
const engineBindingsRef = useRef<Partial<Record<MapProps["mode"], EngineBinding>>>({});
|
||||
const previousModeRef = useRef<MapProps["mode"]>(mode);
|
||||
|
||||
useEffect(() => {
|
||||
modeRef.current = mode;
|
||||
}, [mode]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousMode = previousModeRef.current;
|
||||
if (previousMode !== mode) {
|
||||
engineBindingsRef.current[previousMode]?.cancel?.();
|
||||
previousModeRef.current = mode;
|
||||
}
|
||||
|
||||
const map = mapRef.current;
|
||||
if (!map || !map.isStyleLoaded()) return;
|
||||
if (mode !== "draw") {
|
||||
(map.getSource("draw-preview") as maplibregl.GeoJSONSource | undefined)?.setData({
|
||||
type: "FeatureCollection",
|
||||
features: [],
|
||||
});
|
||||
}
|
||||
if (mode !== "add-line") {
|
||||
(map.getSource("draw-line-preview") as maplibregl.GeoJSONSource | undefined)?.setData({
|
||||
type: "FeatureCollection",
|
||||
@@ -184,6 +204,12 @@ export default function Map({
|
||||
selectedFeatureIdRef.current = selectedFeatureId;
|
||||
}, [selectedFeatureId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== "select" || selectedFeatureId === null) {
|
||||
editingEngineRef.current?.clearEditing();
|
||||
}
|
||||
}, [mode, selectedFeatureId]);
|
||||
|
||||
useEffect(() => {
|
||||
fitBoundsAppliedRef.current = false;
|
||||
}, [fitBoundsKey]);
|
||||
@@ -791,7 +817,7 @@ export default function Map({
|
||||
addPointSymbolLayer(map);
|
||||
|
||||
// init drawing
|
||||
const cleanup = initDrawing(
|
||||
const drawingEngine = initDrawing(
|
||||
map,
|
||||
() => modeRef.current,
|
||||
(geometry: Geometry) => {
|
||||
@@ -813,7 +839,7 @@ export default function Map({
|
||||
}
|
||||
);
|
||||
|
||||
const cleanupSelect = initSelect(
|
||||
const selectEngine = initSelect(
|
||||
map,
|
||||
() => modeRef.current,
|
||||
allowGeometryEditing
|
||||
@@ -852,7 +878,7 @@ export default function Map({
|
||||
}
|
||||
);
|
||||
|
||||
const cleanupLine = initLine(
|
||||
const lineEngine = initLine(
|
||||
map,
|
||||
() => modeRef.current,
|
||||
(geometry: Geometry) => {
|
||||
@@ -874,7 +900,7 @@ export default function Map({
|
||||
}
|
||||
);
|
||||
|
||||
const cleanupPath = initPath(
|
||||
const pathEngine = initPath(
|
||||
map,
|
||||
() => modeRef.current,
|
||||
(geometry: Geometry) => {
|
||||
@@ -896,7 +922,7 @@ export default function Map({
|
||||
}
|
||||
);
|
||||
|
||||
const cleanupCircle = initCircle(
|
||||
const circleEngine = initCircle(
|
||||
map,
|
||||
() => modeRef.current,
|
||||
(geometry: Geometry) => {
|
||||
@@ -918,13 +944,21 @@ export default function Map({
|
||||
}
|
||||
);
|
||||
|
||||
engineBindingsRef.current = {
|
||||
draw: drawingEngine,
|
||||
select: selectEngine,
|
||||
"add-line": lineEngine,
|
||||
"add-path": pathEngine,
|
||||
"add-circle": circleEngine,
|
||||
};
|
||||
|
||||
mapCleanupFnsRef.current = [
|
||||
cleanupCircle,
|
||||
cleanupPath,
|
||||
cleanupLine,
|
||||
circleEngine.cleanup,
|
||||
pathEngine.cleanup,
|
||||
lineEngine.cleanup,
|
||||
cleanupPoint,
|
||||
cleanupSelect,
|
||||
cleanup,
|
||||
selectEngine.cleanup,
|
||||
drawingEngine.cleanup,
|
||||
() => map.off("zoom", syncZoomLevel),
|
||||
];
|
||||
|
||||
@@ -941,6 +975,7 @@ export default function Map({
|
||||
cleanupFn();
|
||||
}
|
||||
mapCleanupFnsRef.current = [];
|
||||
engineBindingsRef.current = {};
|
||||
if (mapRef.current === map) {
|
||||
mapRef.current = null;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
"use client";
|
||||
|
||||
type Props = {
|
||||
minYear: number;
|
||||
maxYear: number;
|
||||
windowStartYear: number;
|
||||
windowEndYear: number;
|
||||
onWindowStartYearChange: (year: number) => void;
|
||||
onWindowEndYearChange: (year: number) => void;
|
||||
year: number;
|
||||
onYearChange: (year: number) => void;
|
||||
isLoading: boolean;
|
||||
@@ -14,34 +8,28 @@ type Props = {
|
||||
statusText?: string | null;
|
||||
};
|
||||
|
||||
const FIXED_TIMELINE_START_YEAR = -2000;
|
||||
const FIXED_TIMELINE_END_YEAR = 2000;
|
||||
|
||||
export default function TimelineBar({
|
||||
minYear,
|
||||
maxYear,
|
||||
windowStartYear,
|
||||
windowEndYear,
|
||||
onWindowStartYearChange,
|
||||
onWindowEndYearChange,
|
||||
year,
|
||||
onYearChange,
|
||||
isLoading,
|
||||
disabled,
|
||||
statusText,
|
||||
}: Props) {
|
||||
const lower = Math.min(minYear, maxYear);
|
||||
const upper = Math.max(minYear, maxYear);
|
||||
const globalLocked = lower === upper;
|
||||
const effectiveDisabled = disabled || globalLocked;
|
||||
const safeWindowStart = clampYear(windowStartYear, lower, upper);
|
||||
const safeWindowEnd = clampYear(windowEndYear, safeWindowStart, upper);
|
||||
const windowLocked = safeWindowStart === safeWindowEnd;
|
||||
const safeYear = clampYear(year, safeWindowStart, safeWindowEnd);
|
||||
const pointDisabled = effectiveDisabled || windowLocked;
|
||||
const windowStartPercent = toPercent(safeWindowStart, lower, upper);
|
||||
const windowEndPercent = toPercent(safeWindowEnd, lower, upper);
|
||||
const lower = FIXED_TIMELINE_START_YEAR;
|
||||
const upper = FIXED_TIMELINE_END_YEAR;
|
||||
const effectiveDisabled = disabled;
|
||||
const safeYear = clampYear(year, lower, upper);
|
||||
|
||||
const helperText = isLoading
|
||||
? "Đang tải geometry theo mốc thời gian..."
|
||||
: statusText || (windowLocked ? "Khoảng lớn đang thu về một mốc duy nhất." : "Kéo mốc nhỏ để query trong khoảng lớn.");
|
||||
: statusText || "Kéo thanh hoặc nhập số năm để query chính xác.";
|
||||
|
||||
const handleYearChange = (nextYear: number) => {
|
||||
onYearChange(clampYear(Math.trunc(nextYear), lower, upper));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -76,86 +64,54 @@ export default function TimelineBar({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: "12px", color: "#cbd5e1", marginBottom: "6px" }}>
|
||||
Khoảng thời gian lớn
|
||||
</div>
|
||||
<div
|
||||
className="dual-range"
|
||||
style={{
|
||||
opacity: effectiveDisabled ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
<div className="dual-range-track" />
|
||||
<div
|
||||
className="dual-range-selected"
|
||||
style={{
|
||||
left: `${windowStartPercent}%`,
|
||||
width: `${Math.max(windowEndPercent - windowStartPercent, 0)}%`,
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className="dual-range-input"
|
||||
type="range"
|
||||
min={lower}
|
||||
max={upper}
|
||||
step={1}
|
||||
value={safeWindowStart}
|
||||
onChange={(event) =>
|
||||
onWindowStartYearChange(
|
||||
Math.min(Number(event.target.value), safeWindowEnd)
|
||||
)
|
||||
}
|
||||
disabled={effectiveDisabled}
|
||||
aria-label="Timeline window start"
|
||||
/>
|
||||
<input
|
||||
className="dual-range-input"
|
||||
type="range"
|
||||
min={lower}
|
||||
max={upper}
|
||||
step={1}
|
||||
value={safeWindowEnd}
|
||||
onChange={(event) =>
|
||||
onWindowEndYearChange(
|
||||
Math.max(Number(event.target.value), safeWindowStart)
|
||||
)
|
||||
}
|
||||
disabled={effectiveDisabled}
|
||||
aria-label="Timeline window end"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: "6px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
fontSize: "12px",
|
||||
color: "#94a3b8",
|
||||
}}
|
||||
>
|
||||
<span>{formatYear(safeWindowStart)}</span>
|
||||
<span>{formatYear(safeWindowEnd)}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: "12px", color: "#cbd5e1", marginTop: "8px", marginBottom: "6px" }}>
|
||||
Mốc thời gian chi tiết
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "minmax(0, 1fr) 120px",
|
||||
alignItems: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
min={safeWindowStart}
|
||||
max={safeWindowEnd}
|
||||
min={lower}
|
||||
max={upper}
|
||||
step={1}
|
||||
value={safeYear}
|
||||
onChange={(event) => onYearChange(Number(event.target.value))}
|
||||
disabled={pointDisabled}
|
||||
onChange={(event) => handleYearChange(Number(event.target.value))}
|
||||
disabled={effectiveDisabled}
|
||||
aria-label="Timeline year"
|
||||
style={{
|
||||
width: "100%",
|
||||
accentColor: "#22c55e",
|
||||
cursor: pointDisabled ? "not-allowed" : "pointer",
|
||||
opacity: pointDisabled ? 0.6 : 1,
|
||||
cursor: effectiveDisabled ? "not-allowed" : "pointer",
|
||||
opacity: effectiveDisabled ? 0.6 : 1,
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={lower}
|
||||
max={upper}
|
||||
step={1}
|
||||
value={safeYear}
|
||||
onChange={(event) => handleYearChange(Number(event.target.value))}
|
||||
disabled={effectiveDisabled}
|
||||
aria-label="Timeline exact year"
|
||||
style={{
|
||||
width: "100%",
|
||||
border: "1px solid rgba(148, 163, 184, 0.45)",
|
||||
borderRadius: "6px",
|
||||
padding: "6px 8px",
|
||||
background: "rgba(15, 23, 42, 0.7)",
|
||||
color: "#f8fafc",
|
||||
fontSize: "13px",
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
@@ -167,85 +123,12 @@ export default function TimelineBar({
|
||||
fontSize: "12px",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "#94a3b8" }}>{formatYear(safeWindowStart)}</span>
|
||||
<span style={{ color: "#94a3b8" }}>{formatYear(lower)}</span>
|
||||
<span style={{ color: "#cbd5e1", textAlign: "center", whiteSpace: "nowrap" }}>
|
||||
{helperText}
|
||||
</span>
|
||||
<span style={{ color: "#94a3b8", textAlign: "right" }}>{formatYear(safeWindowEnd)}</span>
|
||||
<span style={{ color: "#94a3b8", textAlign: "right" }}>{formatYear(upper)}</span>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.dual-range {
|
||||
position: relative;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.dual-range-track {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
background: rgba(148, 163, 184, 0.35);
|
||||
}
|
||||
|
||||
.dual-range-selected {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
background: #22c55e;
|
||||
}
|
||||
|
||||
.dual-range-input {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 22px;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.dual-range-input::-webkit-slider-runnable-track {
|
||||
height: 4px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dual-range-input::-webkit-slider-thumb {
|
||||
pointer-events: auto;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-top: -5px;
|
||||
border-radius: 999px;
|
||||
border: 2px solid #0f172a;
|
||||
background: #22c55e;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dual-range-input::-moz-range-track {
|
||||
height: 4px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.dual-range-input::-moz-range-thumb {
|
||||
pointer-events: auto;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
border: 2px solid #0f172a;
|
||||
background: #22c55e;
|
||||
cursor: pointer;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -262,8 +145,3 @@ function formatYear(year: number): string {
|
||||
}
|
||||
return `${year}`;
|
||||
}
|
||||
|
||||
function toPercent(value: number, minValue: number, maxValue: number): number {
|
||||
if (maxValue <= minValue) return 0;
|
||||
return ((value - minValue) / (maxValue - minValue)) * 100;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user