time query by range | filter by type
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user