time query by range | filter by type

This commit is contained in:
taDuc
2026-05-09 23:02:48 +07:00
parent a9b8c4ab8b
commit 6757eb2086
9 changed files with 172 additions and 15 deletions
@@ -6,12 +6,15 @@ import {
BackgroundLayerId,
BackgroundLayerVisibility,
} from "@/uhm/lib/backgroundLayers";
import { GEO_TYPE_KEYS } from "@/uhm/lib/geoTypeMap";
type Props = {
visibility: BackgroundLayerVisibility;
onToggleLayer: (id: BackgroundLayerId) => void;
onShowAll: () => void;
onHideAll: () => void;
geometryVisibility?: Record<string, boolean>;
onToggleGeometryType?: (typeKey: string) => void;
topContent?: ReactNode;
width?: number;
};
@@ -21,6 +24,8 @@ export default function BackgroundLayersPanel({
onToggleLayer,
onShowAll,
onHideAll,
geometryVisibility,
onToggleGeometryType,
topContent,
width = 240,
}: Props) {
@@ -69,6 +74,45 @@ export default function BackgroundLayersPanel({
);
})}
</div>
{geometryVisibility && onToggleGeometryType ? (
<>
<div style={{ height: 1, background: "#1f2937", margin: "12px 0" }} />
<div style={{ margin: 0, marginBottom: 10, fontWeight: 800, fontSize: 13, color: "#e5e7eb" }}>
Geometries
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "10px 12px" }}>
{GEO_TYPE_KEYS.map((typeKey) => {
const on = geometryVisibility[typeKey] !== false;
return (
<button
key={typeKey}
type="button"
onClick={() => onToggleGeometryType(typeKey)}
style={{
border: "none",
background: "transparent",
padding: 0,
margin: 0,
cursor: "pointer",
color: on ? "#22c55e" : "#e5e7eb",
textDecorationLine: on ? "none" : "line-through",
textDecorationThickness: on ? undefined : "2px",
textDecorationColor: on ? undefined : "rgba(148, 163, 184, 0.7)",
fontSize: 13,
fontWeight: 750,
whiteSpace: "nowrap",
textTransform: "capitalize",
}}
title={on ? "On" : "Off"}
>
{typeKey.replaceAll("_", " ")}
</button>
);
})}
</div>
</>
) : null}
</aside>
);
}
+30 -1
View File
@@ -42,6 +42,7 @@ type MapProps = {
mode: EditorMode;
draft: FeatureCollection;
backgroundVisibility: BackgroundLayerVisibility;
geometryVisibility?: Record<string, boolean>;
selectedFeatureId: string | number | null;
onSelectFeatureId: (id: string | number | null) => void;
onCreateFeature?: (feature: FeatureCollection["features"][number]) => void;
@@ -70,6 +71,7 @@ export default function Map({
mode,
draft,
backgroundVisibility,
geometryVisibility,
selectedFeatureId,
onSelectFeatureId,
onCreateFeature,
@@ -98,6 +100,8 @@ export default function Map({
const draftRef = useRef<FeatureCollection>(draft);
// Mirror của backgroundVisibility để sync visibility khi map đã load.
const backgroundVisibilityRef = useRef<BackgroundLayerVisibility>(backgroundVisibility);
// Mirror of geometry visibility for type filtering.
const geometryVisibilityRef = useRef<MapProps["geometryVisibility"]>(geometryVisibility);
// Mirror của selectedFeatureId để filter/select trên map (không phụ thuộc re-render).
const selectedFeatureIdRef = useRef<string | number | null>(selectedFeatureId);
// Mirror của callback onSelectFeatureId.
@@ -223,6 +227,14 @@ export default function Map({
applyBackgroundLayerVisibility(map, backgroundVisibility);
}, [backgroundVisibility]);
useEffect(() => {
geometryVisibilityRef.current = geometryVisibility;
// When toggling geometry types, refresh sources immediately (without waiting for parent re-mount).
const map = mapRef.current;
if (!map) return;
applyDraftToMap(draftRef.current);
}, [geometryVisibility]);
useEffect(() => {
onCreateRef.current = onCreateFeature;
}, [onCreateFeature]);
@@ -264,9 +276,10 @@ export default function Map({
}
}
const visibleDraft = respectBindingFilterRef.current
const visibleDraftRaw = respectBindingFilterRef.current
? filterDraftByBinding(fc, selectedFeatureIdRef.current)
: fc;
const visibleDraft = filterDraftByGeometryVisibility(visibleDraftRaw, geometryVisibilityRef.current);
const { polygons, points } = splitDraftFeatures(visibleDraft);
const pathArrowShapes = buildPathArrowFeatureCollection(visibleDraft);
@@ -1397,6 +1410,22 @@ function filterDraftByBinding(
};
}
function filterDraftByGeometryVisibility(
fc: FeatureCollection,
visibility: Record<string, boolean> | null | undefined
): FeatureCollection {
if (!visibility) return fc;
return {
...fc,
features: fc.features.filter((feature) => {
const key = getFeatureSemanticType(feature);
if (!key) return true;
return visibility[key] !== false;
}),
};
}
function normalizeBindingIds(rawBinding: unknown): string[] {
if (!Array.isArray(rawBinding)) return [];
const deduped: string[] = [];
+45
View File
@@ -5,6 +5,8 @@ import { FIXED_TIMELINE_END_YEAR, FIXED_TIMELINE_START_YEAR, clampYearValue } fr
type Props = {
year: number;
onYearChange: (year: number) => void;
timeRange?: number;
onTimeRangeChange?: (range: number) => void;
isLoading: boolean;
disabled: boolean;
statusText?: string | null;
@@ -15,6 +17,8 @@ type Props = {
export default function TimelineBar({
year,
onYearChange,
timeRange,
onTimeRangeChange,
isLoading,
disabled,
statusText,
@@ -34,6 +38,12 @@ export default function TimelineBar({
onYearChange(clampYearValue(Math.trunc(nextYear), lower, upper));
};
const handleTimeRangeChange = (nextValue: number) => {
if (!onTimeRangeChange) return;
const safe = Number.isFinite(nextValue) ? Math.trunc(nextValue) : 0;
onTimeRangeChange(Math.max(0, Math.min(30, safe)));
};
return (
<div
style={{
@@ -148,6 +158,41 @@ export default function TimelineBar({
outline: "none",
}}
/>
{typeof timeRange === "number" && onTimeRangeChange ? (
<label
title="time_range (0-30)"
style={{
display: "inline-flex",
alignItems: "center",
gap: 6,
color: "#94a3b8",
whiteSpace: "nowrap",
opacity: effectiveDisabled ? 0.6 : 1,
}}
>
<span style={{ fontSize: "12px" }}>Range</span>
<input
type="number"
min={0}
max={30}
step={1}
value={Math.max(0, Math.min(30, Math.trunc(timeRange)))}
onChange={(event) => handleTimeRangeChange(Number(event.target.value))}
disabled={effectiveDisabled}
aria-label="Timeline range"
style={{
width: "84px",
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",
}}
/>
</label>
) : null}
</div>
</div>
);