time query by range | filter by type
This commit is contained in:
@@ -23,7 +23,7 @@ import {
|
|||||||
Geometry,
|
Geometry,
|
||||||
useEditorState,
|
useEditorState,
|
||||||
} from "@/uhm/lib/useEditorState";
|
} from "@/uhm/lib/useEditorState";
|
||||||
import { geoTypeCodeToTypeKey } from "@/uhm/lib/geoTypeMap";
|
import { GEO_TYPE_KEYS, geoTypeCodeToTypeKey } from "@/uhm/lib/geoTypeMap";
|
||||||
import {
|
import {
|
||||||
BackgroundLayerId,
|
BackgroundLayerId,
|
||||||
BackgroundLayerVisibility,
|
BackgroundLayerVisibility,
|
||||||
@@ -167,6 +167,12 @@ export default function Page() {
|
|||||||
const wikiSearchRequestRef = useRef(0);
|
const wikiSearchRequestRef = useRef(0);
|
||||||
const geoSearchRequestRef = useRef(0);
|
const geoSearchRequestRef = useRef(0);
|
||||||
|
|
||||||
|
const [geometryVisibility, setGeometryVisibility] = useState<Record<string, boolean>>(() => {
|
||||||
|
const init: Record<string, boolean> = {};
|
||||||
|
for (const key of GEO_TYPE_KEYS) init[key] = true;
|
||||||
|
return init;
|
||||||
|
});
|
||||||
|
|
||||||
const snapshotEntitiesRef = useRef(snapshotEntities);
|
const snapshotEntitiesRef = useRef(snapshotEntities);
|
||||||
const snapshotWikisRef = useRef(snapshotWikis);
|
const snapshotWikisRef = useRef(snapshotWikis);
|
||||||
const snapshotEntityWikiLinksRef = useRef(snapshotEntityWikiLinks);
|
const snapshotEntityWikiLinksRef = useRef(snapshotEntityWikiLinks);
|
||||||
@@ -1211,6 +1217,7 @@ export default function Page() {
|
|||||||
onDeleteFeature={editor.deleteFeature}
|
onDeleteFeature={editor.deleteFeature}
|
||||||
onUpdateFeature={editor.updateFeature}
|
onUpdateFeature={editor.updateFeature}
|
||||||
backgroundVisibility={backgroundVisibility}
|
backgroundVisibility={backgroundVisibility}
|
||||||
|
geometryVisibility={geometryVisibility}
|
||||||
respectBindingFilter={geometryBindingFilterEnabled}
|
respectBindingFilter={geometryBindingFilterEnabled}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@@ -1244,6 +1251,10 @@ export default function Page() {
|
|||||||
onToggleLayer={handleToggleBackgroundLayer}
|
onToggleLayer={handleToggleBackgroundLayer}
|
||||||
onShowAll={handleShowAllBackgroundLayers}
|
onShowAll={handleShowAllBackgroundLayers}
|
||||||
onHideAll={handleHideAllBackgroundLayers}
|
onHideAll={handleHideAllBackgroundLayers}
|
||||||
|
geometryVisibility={geometryVisibility}
|
||||||
|
onToggleGeometryType={(typeKey) => {
|
||||||
|
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" }}>
|
||||||
|
|||||||
+21
-2
@@ -19,6 +19,7 @@ import {
|
|||||||
} from "@/uhm/lib/editor/background/backgroundVisibilityStorage";
|
} from "@/uhm/lib/editor/background/backgroundVisibilityStorage";
|
||||||
import { EMPTY_FEATURE_COLLECTION, WORLD_BBOX } from "@/uhm/lib/geo/constants";
|
import { EMPTY_FEATURE_COLLECTION, WORLD_BBOX } from "@/uhm/lib/geo/constants";
|
||||||
import { clampYearToFixedRange, TIMELINE_DEBOUNCE_MS } from "@/uhm/lib/timeline";
|
import { clampYearToFixedRange, TIMELINE_DEBOUNCE_MS } from "@/uhm/lib/timeline";
|
||||||
|
import { GEO_TYPE_KEYS } from "@/uhm/lib/geoTypeMap";
|
||||||
import type { Feature, FeatureCollection } from "@/uhm/types/geo";
|
import type { Feature, FeatureCollection } from "@/uhm/types/geo";
|
||||||
|
|
||||||
const CURRENT_YEAR = new Date().getUTCFullYear();
|
const CURRENT_YEAR = new Date().getUTCFullYear();
|
||||||
@@ -28,6 +29,7 @@ export default function Page() {
|
|||||||
const [selectedFeatureId, setSelectedFeatureId] = useState<string | number | null>(null);
|
const [selectedFeatureId, setSelectedFeatureId] = useState<string | number | null>(null);
|
||||||
const [timelineYear, setTimelineYear] = useState<number>(() => clampYearToFixedRange(CURRENT_YEAR));
|
const [timelineYear, setTimelineYear] = useState<number>(() => clampYearToFixedRange(CURRENT_YEAR));
|
||||||
const [timelineDraftYear, setTimelineDraftYear] = useState<number>(() => clampYearToFixedRange(CURRENT_YEAR));
|
const [timelineDraftYear, setTimelineDraftYear] = useState<number>(() => clampYearToFixedRange(CURRENT_YEAR));
|
||||||
|
const [timeRange, setTimeRange] = useState<number>(0);
|
||||||
const [isTimelineLoading, setIsTimelineLoading] = useState(false);
|
const [isTimelineLoading, setIsTimelineLoading] = useState(false);
|
||||||
const [timelineStatus, setTimelineStatus] = useState<string | null>(null);
|
const [timelineStatus, setTimelineStatus] = useState<string | null>(null);
|
||||||
const [backgroundVisibility, setBackgroundVisibility] = useState<BackgroundLayerVisibility>(
|
const [backgroundVisibility, setBackgroundVisibility] = useState<BackgroundLayerVisibility>(
|
||||||
@@ -36,6 +38,11 @@ export default function Page() {
|
|||||||
const [isBackgroundVisibilityReady, setIsBackgroundVisibilityReady] = useState(false);
|
const [isBackgroundVisibilityReady, setIsBackgroundVisibilityReady] = useState(false);
|
||||||
const timelineFetchRequestRef = useRef(0);
|
const timelineFetchRequestRef = useRef(0);
|
||||||
const [lastLoadedAt, setLastLoadedAt] = useState<string | null>(null);
|
const [lastLoadedAt, setLastLoadedAt] = useState<string | null>(null);
|
||||||
|
const [geometryVisibility, setGeometryVisibility] = useState<Record<string, boolean>>(() => {
|
||||||
|
const init: Record<string, boolean> = {};
|
||||||
|
for (const key of GEO_TYPE_KEYS) init[key] = true;
|
||||||
|
return init;
|
||||||
|
});
|
||||||
|
|
||||||
const selectedFeature: Feature | null = useMemo(() => {
|
const selectedFeature: Feature | null = useMemo(() => {
|
||||||
if (selectedFeatureId === null) return null;
|
if (selectedFeatureId === null) return null;
|
||||||
@@ -70,7 +77,7 @@ export default function Page() {
|
|||||||
setIsTimelineLoading(true);
|
setIsTimelineLoading(true);
|
||||||
setTimelineStatus(null);
|
setTimelineStatus(null);
|
||||||
try {
|
try {
|
||||||
const next = await fetchGeometriesByBBox({ ...WORLD_BBOX, time: timelineYear });
|
const next = await fetchGeometriesByBBox({ ...WORLD_BBOX, time: timelineYear, timeRange });
|
||||||
if (disposed || requestId !== timelineFetchRequestRef.current) return;
|
if (disposed || requestId !== timelineFetchRequestRef.current) return;
|
||||||
setData(next);
|
setData(next);
|
||||||
setLastLoadedAt(new Date().toISOString());
|
setLastLoadedAt(new Date().toISOString());
|
||||||
@@ -94,7 +101,7 @@ export default function Page() {
|
|||||||
return () => {
|
return () => {
|
||||||
disposed = true;
|
disposed = true;
|
||||||
};
|
};
|
||||||
}, [timelineYear]);
|
}, [timelineYear, timeRange]);
|
||||||
|
|
||||||
const updateBackgroundVisibility = (updater: (prev: BackgroundLayerVisibility) => BackgroundLayerVisibility) => {
|
const updateBackgroundVisibility = (updater: (prev: BackgroundLayerVisibility) => BackgroundLayerVisibility) => {
|
||||||
setBackgroundVisibility((prev) => {
|
setBackgroundVisibility((prev) => {
|
||||||
@@ -120,6 +127,11 @@ export default function Page() {
|
|||||||
setTimelineDraftYear(clampYearToFixedRange(Math.trunc(nextYear)));
|
setTimelineDraftYear(clampYearToFixedRange(Math.trunc(nextYear)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTimeRangeChange = (nextRange: number) => {
|
||||||
|
const safe = Number.isFinite(nextRange) ? Math.trunc(nextRange) : 0;
|
||||||
|
setTimeRange(Math.max(0, Math.min(30, safe)));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", minHeight: "100vh" }}>
|
<div style={{ display: "flex", minHeight: "100vh" }}>
|
||||||
<div style={{ flex: 1, position: "relative", minHeight: "100vh" }}>
|
<div style={{ flex: 1, position: "relative", minHeight: "100vh" }}>
|
||||||
@@ -130,6 +142,7 @@ export default function Page() {
|
|||||||
selectedFeatureId={selectedFeatureId}
|
selectedFeatureId={selectedFeatureId}
|
||||||
onSelectFeatureId={setSelectedFeatureId}
|
onSelectFeatureId={setSelectedFeatureId}
|
||||||
backgroundVisibility={backgroundVisibility}
|
backgroundVisibility={backgroundVisibility}
|
||||||
|
geometryVisibility={geometryVisibility}
|
||||||
allowGeometryEditing={false}
|
allowGeometryEditing={false}
|
||||||
respectBindingFilter={false}
|
respectBindingFilter={false}
|
||||||
/>
|
/>
|
||||||
@@ -140,6 +153,8 @@ export default function Page() {
|
|||||||
<TimelineBar
|
<TimelineBar
|
||||||
year={timelineDraftYear}
|
year={timelineDraftYear}
|
||||||
onYearChange={handleTimelineYearChange}
|
onYearChange={handleTimelineYearChange}
|
||||||
|
timeRange={timeRange}
|
||||||
|
onTimeRangeChange={handleTimeRangeChange}
|
||||||
isLoading={isTimelineLoading}
|
isLoading={isTimelineLoading}
|
||||||
disabled={false}
|
disabled={false}
|
||||||
statusText={timelineStatus}
|
statusText={timelineStatus}
|
||||||
@@ -151,6 +166,10 @@ export default function Page() {
|
|||||||
onToggleLayer={handleToggleBackgroundLayer}
|
onToggleLayer={handleToggleBackgroundLayer}
|
||||||
onShowAll={handleShowAllBackgroundLayers}
|
onShowAll={handleShowAllBackgroundLayers}
|
||||||
onHideAll={handleHideAllBackgroundLayers}
|
onHideAll={handleHideAllBackgroundLayers}
|
||||||
|
geometryVisibility={geometryVisibility}
|
||||||
|
onToggleGeometryType={(typeKey) => {
|
||||||
|
setGeometryVisibility((prev) => ({ ...prev, [typeKey]: prev[typeKey] === false }));
|
||||||
|
}}
|
||||||
topContent={
|
topContent={
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import React, { ReactNode } from "react";
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
interface ButtonProps {
|
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||||
children: ReactNode; // Button text or content
|
|
||||||
size?: "sm" | "md"; // Button size
|
size?: "sm" | "md"; // Button size
|
||||||
variant?: "primary" | "outline"; // Button variant
|
variant?: "primary" | "outline"; // Button variant
|
||||||
startIcon?: ReactNode; // Icon before the text
|
startIcon?: ReactNode; // Icon before the text
|
||||||
endIcon?: ReactNode; // Icon after the text
|
endIcon?: ReactNode; // Icon after the text
|
||||||
onClick?: () => void; // Click handler
|
};
|
||||||
disabled?: boolean; // Disabled state
|
|
||||||
className?: string; // Disabled state
|
|
||||||
type?: "button" | "submit" | "reset";
|
|
||||||
}
|
|
||||||
|
|
||||||
const Button: React.FC<ButtonProps> = ({
|
const Button: React.FC<ButtonProps> = ({
|
||||||
children,
|
children,
|
||||||
@@ -18,10 +13,10 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
variant = "primary",
|
variant = "primary",
|
||||||
startIcon,
|
startIcon,
|
||||||
endIcon,
|
endIcon,
|
||||||
onClick,
|
|
||||||
className = "",
|
className = "",
|
||||||
disabled = false,
|
disabled = false,
|
||||||
type = "button",
|
type = "button",
|
||||||
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
// Size Classes
|
// Size Classes
|
||||||
const sizeClasses = {
|
const sizeClasses = {
|
||||||
@@ -44,8 +39,9 @@ const Button: React.FC<ButtonProps> = ({
|
|||||||
} ${variantClasses[variant]} ${
|
} ${variantClasses[variant]} ${
|
||||||
disabled ? "cursor-not-allowed opacity-50" : ""
|
disabled ? "cursor-not-allowed opacity-50" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
type={type}
|
||||||
|
{...rest}
|
||||||
>
|
>
|
||||||
{startIcon && <span className="flex items-center">{startIcon}</span>}
|
{startIcon && <span className="flex items-center">{startIcon}</span>}
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ function buildBBoxQueryString(params: GeometriesBBoxQuery): string {
|
|||||||
query.set("time", String(params.time));
|
query.set("time", String(params.time));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (params.timeRange !== undefined) {
|
||||||
|
query.set("time_range", String(params.timeRange));
|
||||||
|
}
|
||||||
|
|
||||||
if (params.entity_id) {
|
if (params.entity_id) {
|
||||||
query.set("entity_id", params.entity_id);
|
query.set("entity_id", params.entity_id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import {
|
|||||||
BackgroundLayerId,
|
BackgroundLayerId,
|
||||||
BackgroundLayerVisibility,
|
BackgroundLayerVisibility,
|
||||||
} from "@/uhm/lib/backgroundLayers";
|
} from "@/uhm/lib/backgroundLayers";
|
||||||
|
import { GEO_TYPE_KEYS } from "@/uhm/lib/geoTypeMap";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
visibility: BackgroundLayerVisibility;
|
visibility: BackgroundLayerVisibility;
|
||||||
onToggleLayer: (id: BackgroundLayerId) => void;
|
onToggleLayer: (id: BackgroundLayerId) => void;
|
||||||
onShowAll: () => void;
|
onShowAll: () => void;
|
||||||
onHideAll: () => void;
|
onHideAll: () => void;
|
||||||
|
geometryVisibility?: Record<string, boolean>;
|
||||||
|
onToggleGeometryType?: (typeKey: string) => void;
|
||||||
topContent?: ReactNode;
|
topContent?: ReactNode;
|
||||||
width?: number;
|
width?: number;
|
||||||
};
|
};
|
||||||
@@ -21,6 +24,8 @@ export default function BackgroundLayersPanel({
|
|||||||
onToggleLayer,
|
onToggleLayer,
|
||||||
onShowAll,
|
onShowAll,
|
||||||
onHideAll,
|
onHideAll,
|
||||||
|
geometryVisibility,
|
||||||
|
onToggleGeometryType,
|
||||||
topContent,
|
topContent,
|
||||||
width = 240,
|
width = 240,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
@@ -69,6 +74,45 @@ export default function BackgroundLayersPanel({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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>
|
</aside>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type MapProps = {
|
|||||||
mode: EditorMode;
|
mode: EditorMode;
|
||||||
draft: FeatureCollection;
|
draft: FeatureCollection;
|
||||||
backgroundVisibility: BackgroundLayerVisibility;
|
backgroundVisibility: BackgroundLayerVisibility;
|
||||||
|
geometryVisibility?: Record<string, boolean>;
|
||||||
selectedFeatureId: string | number | null;
|
selectedFeatureId: string | number | null;
|
||||||
onSelectFeatureId: (id: string | number | null) => void;
|
onSelectFeatureId: (id: string | number | null) => void;
|
||||||
onCreateFeature?: (feature: FeatureCollection["features"][number]) => void;
|
onCreateFeature?: (feature: FeatureCollection["features"][number]) => void;
|
||||||
@@ -70,6 +71,7 @@ export default function Map({
|
|||||||
mode,
|
mode,
|
||||||
draft,
|
draft,
|
||||||
backgroundVisibility,
|
backgroundVisibility,
|
||||||
|
geometryVisibility,
|
||||||
selectedFeatureId,
|
selectedFeatureId,
|
||||||
onSelectFeatureId,
|
onSelectFeatureId,
|
||||||
onCreateFeature,
|
onCreateFeature,
|
||||||
@@ -98,6 +100,8 @@ export default function Map({
|
|||||||
const draftRef = useRef<FeatureCollection>(draft);
|
const draftRef = useRef<FeatureCollection>(draft);
|
||||||
// Mirror của backgroundVisibility để sync visibility khi map đã load.
|
// Mirror của backgroundVisibility để sync visibility khi map đã load.
|
||||||
const backgroundVisibilityRef = useRef<BackgroundLayerVisibility>(backgroundVisibility);
|
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).
|
// Mirror của selectedFeatureId để filter/select trên map (không phụ thuộc re-render).
|
||||||
const selectedFeatureIdRef = useRef<string | number | null>(selectedFeatureId);
|
const selectedFeatureIdRef = useRef<string | number | null>(selectedFeatureId);
|
||||||
// Mirror của callback onSelectFeatureId.
|
// Mirror của callback onSelectFeatureId.
|
||||||
@@ -223,6 +227,14 @@ export default function Map({
|
|||||||
applyBackgroundLayerVisibility(map, backgroundVisibility);
|
applyBackgroundLayerVisibility(map, backgroundVisibility);
|
||||||
}, [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(() => {
|
useEffect(() => {
|
||||||
onCreateRef.current = onCreateFeature;
|
onCreateRef.current = onCreateFeature;
|
||||||
}, [onCreateFeature]);
|
}, [onCreateFeature]);
|
||||||
@@ -264,9 +276,10 @@ export default function Map({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleDraft = respectBindingFilterRef.current
|
const visibleDraftRaw = respectBindingFilterRef.current
|
||||||
? filterDraftByBinding(fc, selectedFeatureIdRef.current)
|
? filterDraftByBinding(fc, selectedFeatureIdRef.current)
|
||||||
: fc;
|
: fc;
|
||||||
|
const visibleDraft = filterDraftByGeometryVisibility(visibleDraftRaw, geometryVisibilityRef.current);
|
||||||
const { polygons, points } = splitDraftFeatures(visibleDraft);
|
const { polygons, points } = splitDraftFeatures(visibleDraft);
|
||||||
const pathArrowShapes = buildPathArrowFeatureCollection(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[] {
|
function normalizeBindingIds(rawBinding: unknown): string[] {
|
||||||
if (!Array.isArray(rawBinding)) return [];
|
if (!Array.isArray(rawBinding)) return [];
|
||||||
const deduped: string[] = [];
|
const deduped: string[] = [];
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { FIXED_TIMELINE_END_YEAR, FIXED_TIMELINE_START_YEAR, clampYearValue } fr
|
|||||||
type Props = {
|
type Props = {
|
||||||
year: number;
|
year: number;
|
||||||
onYearChange: (year: number) => void;
|
onYearChange: (year: number) => void;
|
||||||
|
timeRange?: number;
|
||||||
|
onTimeRangeChange?: (range: number) => void;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
statusText?: string | null;
|
statusText?: string | null;
|
||||||
@@ -15,6 +17,8 @@ type Props = {
|
|||||||
export default function TimelineBar({
|
export default function TimelineBar({
|
||||||
year,
|
year,
|
||||||
onYearChange,
|
onYearChange,
|
||||||
|
timeRange,
|
||||||
|
onTimeRangeChange,
|
||||||
isLoading,
|
isLoading,
|
||||||
disabled,
|
disabled,
|
||||||
statusText,
|
statusText,
|
||||||
@@ -34,6 +38,12 @@ export default function TimelineBar({
|
|||||||
onYearChange(clampYearValue(Math.trunc(nextYear), lower, upper));
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -148,6 +158,41 @@ export default function TimelineBar({
|
|||||||
outline: "none",
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ export type GeoTypeMapRow = {
|
|||||||
|
|
||||||
const MAP_ROWS: GeoTypeMapRow[] = rows as GeoTypeMapRow[];
|
const MAP_ROWS: GeoTypeMapRow[] = rows as GeoTypeMapRow[];
|
||||||
|
|
||||||
|
export const GEO_TYPE_KEYS: string[] = Array.from(
|
||||||
|
new Set(
|
||||||
|
MAP_ROWS
|
||||||
|
.map((row) => (typeof row?.type_key === "string" ? row.type_key.trim().toLowerCase() : ""))
|
||||||
|
.filter((key) => key.length > 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
const CODE_BY_KEY = new Map<string, number>();
|
const CODE_BY_KEY = new Map<string, number>();
|
||||||
const KEY_BY_CODE = new Map<number, string>();
|
const KEY_BY_CODE = new Map<number, string>();
|
||||||
|
|
||||||
@@ -31,4 +39,3 @@ export function geoTypeCodeToTypeKey(code: number | null | undefined): string |
|
|||||||
if (!Number.isFinite(code)) return null;
|
if (!Number.isFinite(code)) return null;
|
||||||
return KEY_BY_CODE.get(Math.trunc(code)) ?? null;
|
return KEY_BY_CODE.get(Math.trunc(code)) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,5 +14,7 @@ export type GeometriesBBoxQuery = {
|
|||||||
maxLng: number;
|
maxLng: number;
|
||||||
maxLat: number;
|
maxLat: number;
|
||||||
time?: number;
|
time?: number;
|
||||||
|
// New API: time_range (0-30) to expand the timeline query window.
|
||||||
|
timeRange?: number;
|
||||||
entity_id?: string;
|
entity_id?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user