add title

This commit is contained in:
tsuki 2025-08-09 22:24:11 +08:00
parent 9646e5d0e1
commit 8d456dacf7
5 changed files with 108 additions and 40 deletions

View File

@ -9,6 +9,7 @@ interface MapContextType {
layers: React.RefObject<any[]>
mapState: MapState
currentDatetime: Date | null
timelineDatetime: Date | null
setMap: (map: Map, layers: any[]) => void
flyTo: (options: { center: [number, number]; zoom: number; duration?: number }) => void
zoomIn: () => void
@ -17,6 +18,7 @@ interface MapContextType {
reset: () => void
clearMap: () => void
setTime: (date: Date) => void
setTimelineTime: (date: Date) => void
isMapReady: boolean
}
@ -44,6 +46,7 @@ export function MapProvider({ children }: MapProviderProps) {
const layersRef = useRef<any[]>([])
const [currentDatetime, setCurrentDatetime] = useState<Date | null>(null)
const [timelineDatetime, setTimelineDatetime] = useState<Date | null>(null)
const setMap = (map: Map, layers: any[]) => {
// 如果已经有地图实例,先清理旧的
@ -131,6 +134,8 @@ export function MapProvider({ children }: MapProviderProps) {
}
const value: MapContextType = {
timelineDatetime,
setTimelineTime: setTimelineDatetime,
setTime,
currentDatetime,
mapRef,

View File

@ -18,15 +18,15 @@ import { ThemeToggle } from '@/components/theme-toggle';
// import { Timeline } from '@/app/timeline';
import { Timeline } from '@/app/tl';
import { cn } from '@/lib/utils';
import { useMap } from './map-context'
import { format } from 'date-fns'
import { Label } from '@/components/ui/label'
export default function Page() {
const now = new Date();
const startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); // 7天前
const endDate = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); // 3天后
const { currentDatetime, timelineDatetime } = useMap()
return (
<SidebarProvider>
@ -36,7 +36,13 @@ export default function Page() {
{/* <SidebarTrigger className="-ml-1" /> */}
{/* <Separator orientation="vertical" className="mr-2 h-4" /> */}
<div className="flex items-center gap-2 justify-between w-full">
{/* <ThemeToggle /> */}
{
currentDatetime && (
<Label>
Real Time: {format(currentDatetime, 'yyyy-MM-dd HH:mm:ss')}
</Label>
)
}
</div>
</header>
<div className="relative h-full w-full flex flex-col">

View File

@ -3,7 +3,7 @@ import vsSource from './glsl/timeline/vert.glsl';
import fsSource from './glsl/timeline/frag.glsl';
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { ChevronLeft, ChevronRight, HomeIcon, LockIcon, Pause, Play, UnlockIcon } from "lucide-react";
import { ChevronLeft, ChevronRight, HomeIcon, LockIcon, Pause, Play, RefreshCwIcon, UnlockIcon } from "lucide-react";
import { formatInTimeZone } from "date-fns-tz";
import { parse } from "date-fns"
@ -79,7 +79,7 @@ export const Timeline: React.FC<Props> = React.memo(({
timelineConfig,
...props
}) => {
const { isPlaying, togglePlay, currentDatetime, setTime } = useTimeline({})
const { isPlaying, togglePlay, currentDatetime, setTime, setTimelineTime, timelineDatetime } = useTimeline({})
const [lock, setLock] = useState(false)
const canvasRef = useRef<HTMLCanvasElement>(null);
const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
@ -101,12 +101,18 @@ export const Timeline: React.FC<Props> = React.memo(({
if (!lock && data.statusUpdates.newestDt) {
const newDt = parse(data.statusUpdates.newestDt + 'Z', 'yyyyMMddHHmmssX', new Date())
setTime(newDt)
setTimelineTime(newDt)
if (timelineEngineRef.current) {
timelineEngineRef.current.replaceTimeMarkByTimestamp(newDt.getTime())
}
}
} else {
}
}
}, [data, lock])
// 定时器效果 - 当播放时每隔指定时间执行操作
useEffect(() => {
let intervalId: NodeJS.Timeout | null = null;
@ -127,12 +133,6 @@ export const Timeline: React.FC<Props> = React.memo(({
};
}, [isPlaying, timeStep]);
useEffect(() => {
if (currentDatetime && !lock) {
timelineEngineRef.current?.replaceTimeMarkByTimestamp(currentDatetime.getTime())
}
}, [currentDatetime, lock])
useEffect(() => {
if (!ticksCanvasRef.current) return;
@ -171,11 +171,11 @@ export const Timeline: React.FC<Props> = React.memo(({
const datestr = formatInTimeZone(date, 'UTC', 'yyyyMMddHHmmss')
const response = await fetch(`http://localhost:3050/api/v1/data/nearest?datetime=${datestr}&area=cn`)
setTimelineTime(date)
if (response.ok) {
const data = await response.json()
const nearestDatetime = data.nearest_data_time
const nearestDate = new Date(Date.parse(nearestDatetime))
setTime(nearestDate)
} else {
console.error('Failed to fetch data:', response.status)
@ -242,6 +242,26 @@ export const Timeline: React.FC<Props> = React.memo(({
}
}, [timeStep]);
const handleRefresh = useCallback(async () => {
if (timelineEngineRef.current) {
const date = new Date()
const datestr = formatInTimeZone(date, 'UTC', 'yyyyMMddHHmmss')
const response = await fetch(`http://localhost:3050/api/v1/data/nearest?datetime=${datestr}&area=cn`)
if (response.ok) {
const data = await response.json()
const nearestDatetime = data.nearest_data_time
const nearestDate = new Date(Date.parse(nearestDatetime))
setTime(nearestDate)
timelineEngineRef.current.replaceTimeMarkByTimestamp(nearestDate.getTime())
timelineEngineRef.current.ensureMarkInView()
} else {
console.error('Failed to fetch data:', response.status)
}
}
}, []);
return (
<div className={cn(props.className, "w-full h-10 flex flex-row")}>
<div className="h-full flex flex-row items-center px-3 gap-2 bg-black/60" style={{ boxShadow: '8px 0 24px rgba(0, 0, 0, 0.15), 4px 0 12px rgba(0, 0, 0, 0.1)' }}>
@ -301,10 +321,8 @@ export const Timeline: React.FC<Props> = React.memo(({
style={{ fontSize: '10px' }}
>
<option value="60000">1</option>
<option value="300000">5</option>
<option value="360000">6</option>
<option value="600000">10</option>
<option value="900000">15</option>
<option value="1800000">30</option>
<option value="3600000">1</option>
<option value="7200000">2</option>
@ -322,7 +340,17 @@ export const Timeline: React.FC<Props> = React.memo(({
{lock ? <LockIcon size={10} /> : <UnlockIcon size={10} />}
</Button>
<Button
variant="secondary"
size="icon"
className="size-5"
onClick={() => {
handleRefresh()
}}
title="刷新"
>
<RefreshCwIcon size={10} />
</Button>
</div>

View File

@ -83,49 +83,76 @@ export function MapComponent({
}
const vertexSource = `#version 300 es
layout(location = 0) in vec2 a_pos;
layout(location = 1) in vec2 a_tex_coord;
${shaderData.vertexShaderPrelude}
${shaderData.define}
out vec2 v_tex_coord;
out vec2 v_merc; // 归一化墨卡托坐标 (0..1)
void main() {
gl_Position = projectTile(a_pos);
v_tex_coord = a_tex_coord;
}`;
// 用 MapLibre 注入的 u_projection_tile_mercator_coords 把 tile 坐标变为 mercator 归一坐标
// merc = offset.xy + scale.zw * a_pos
v_merc = u_projection_tile_mercator_coords.xy +
u_projection_tile_mercator_coords.zw * a_pos;
}
`
// WebGL2 fragment shader
const fragmentSource = `#version 300 es
precision highp float;
uniform sampler2D u_tex;
uniform sampler2D u_lut;
in vec2 v_merc; // 来自 VS 的归一化墨卡托
out vec4 fragColor;
in vec2 v_tex_coord;
const float PI = 3.141592653589793;
// 假设 bound = vec4(west, south, east, north) in degrees
const vec4 bound = vec4(65.24686921730095, 11.90274236858339,
138.85323419021077, 55.34323805611308);
// mercator -> (lon, lat) in radians
vec2 mercatorToLonLat(vec2 merc) {
float lon = merc.x * 2.0 * PI - PI; // 修正这里:-PI
float lat = 2.0 * atan(exp(PI - merc.y * 2.0 * PI)) - 0.5 * PI;
return vec2(lon, lat);
}
vec2 rad2deg(vec2 r) { return r * (180.0 / PI); }
void main() {
vec4 texColor = texture(u_tex, v_tex_coord);
// 对于灰度图RGB通道通常相同取红色通道作为灰度值
float value = texColor.r * 3.4;
// 归一化墨卡托 -> 经纬度(度)
vec2 lonlat = rad2deg(mercatorToLonLat(v_merc));
if (value < 0.07) {
fragColor= vec4(1.0,1.0,1.0,0.2);
return;
// 经纬度 -> 纹理UV按 bound 线性映射)
vec2 uv = vec2(
(lonlat.x - bound.x) / (bound.z - bound.x),
1.0-(lonlat.y - bound.y) / (bound.w - bound.y)
);
// 如果超出范围直接丢弃,避免采到边界垃圾
if(any(lessThan(uv, vec2(0.0))) || any(greaterThan(uv, vec2(1.0)))) {
discard;
}
// normalizedValue = clamp(normalizedValue, 0.0, 1.0);
vec4 texColor = texture(u_tex, uv);
float value = texColor.r * 3.4;
value = clamp(value, 0.0, 1.0);
// 软阈值,避免全场被 return
float alpha = smoothstep(0.07, 0.12, value) * 0.9;
if (alpha <= 0.001) discard;
// 使用 LUT 进行颜色映射
vec4 lutColor = texture(u_lut, vec2(value, 0.5));
// 添加一些透明度,使低值区域更透明
// float alpha = smoothstep(0.0, 0.1, value);
float alpha = 0.7;
fragColor = vec4(lutColor.rgb, alpha);
// fragColor = vec4(1.0,1.0,1.0,0.2);
}`
}
`;
console.log(vertexSource, fragmentSource)

View File

@ -34,7 +34,7 @@ export function useTimeline({
const [isPlaying, setIsPlaying] = useState(autoPlay)
const [speed, setSpeed] = useState<'slow' | 'normal' | 'fast'>('normal')
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const { setTime, currentDatetime } = useMap()
const { setTime, currentDatetime, setTimelineTime, timelineDatetime } = useMap()
const updateDate = useCallback((newDate: Date) => {
setTime(newDate)
@ -117,6 +117,8 @@ export function useTimeline({
}, [])
return {
timelineDatetime,
setTimelineTime,
currentDatetime,
isPlaying,
speed,