add title
This commit is contained in:
parent
9646e5d0e1
commit
8d456dacf7
@ -9,6 +9,7 @@ interface MapContextType {
|
|||||||
layers: React.RefObject<any[]>
|
layers: React.RefObject<any[]>
|
||||||
mapState: MapState
|
mapState: MapState
|
||||||
currentDatetime: Date | null
|
currentDatetime: Date | null
|
||||||
|
timelineDatetime: Date | null
|
||||||
setMap: (map: Map, layers: any[]) => void
|
setMap: (map: Map, layers: any[]) => void
|
||||||
flyTo: (options: { center: [number, number]; zoom: number; duration?: number }) => void
|
flyTo: (options: { center: [number, number]; zoom: number; duration?: number }) => void
|
||||||
zoomIn: () => void
|
zoomIn: () => void
|
||||||
@ -17,6 +18,7 @@ interface MapContextType {
|
|||||||
reset: () => void
|
reset: () => void
|
||||||
clearMap: () => void
|
clearMap: () => void
|
||||||
setTime: (date: Date) => void
|
setTime: (date: Date) => void
|
||||||
|
setTimelineTime: (date: Date) => void
|
||||||
isMapReady: boolean
|
isMapReady: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,6 +46,7 @@ export function MapProvider({ children }: MapProviderProps) {
|
|||||||
|
|
||||||
const layersRef = useRef<any[]>([])
|
const layersRef = useRef<any[]>([])
|
||||||
const [currentDatetime, setCurrentDatetime] = useState<Date | null>(null)
|
const [currentDatetime, setCurrentDatetime] = useState<Date | null>(null)
|
||||||
|
const [timelineDatetime, setTimelineDatetime] = useState<Date | null>(null)
|
||||||
|
|
||||||
const setMap = (map: Map, layers: any[]) => {
|
const setMap = (map: Map, layers: any[]) => {
|
||||||
// 如果已经有地图实例,先清理旧的
|
// 如果已经有地图实例,先清理旧的
|
||||||
@ -131,6 +134,8 @@ export function MapProvider({ children }: MapProviderProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const value: MapContextType = {
|
const value: MapContextType = {
|
||||||
|
timelineDatetime,
|
||||||
|
setTimelineTime: setTimelineDatetime,
|
||||||
setTime,
|
setTime,
|
||||||
currentDatetime,
|
currentDatetime,
|
||||||
mapRef,
|
mapRef,
|
||||||
|
|||||||
16
app/page.tsx
16
app/page.tsx
@ -18,15 +18,15 @@ import { ThemeToggle } from '@/components/theme-toggle';
|
|||||||
// import { Timeline } from '@/app/timeline';
|
// import { Timeline } from '@/app/timeline';
|
||||||
import { Timeline } from '@/app/tl';
|
import { Timeline } from '@/app/tl';
|
||||||
import { cn } from '@/lib/utils';
|
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() {
|
export default function Page() {
|
||||||
|
|
||||||
|
const { currentDatetime, timelineDatetime } = useMap()
|
||||||
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天后
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarProvider>
|
<SidebarProvider>
|
||||||
@ -36,7 +36,13 @@ export default function Page() {
|
|||||||
{/* <SidebarTrigger className="-ml-1" /> */}
|
{/* <SidebarTrigger className="-ml-1" /> */}
|
||||||
{/* <Separator orientation="vertical" className="mr-2 h-4" /> */}
|
{/* <Separator orientation="vertical" className="mr-2 h-4" /> */}
|
||||||
<div className="flex items-center gap-2 justify-between w-full">
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="relative h-full w-full flex flex-col">
|
<div className="relative h-full w-full flex flex-col">
|
||||||
|
|||||||
52
app/tl.tsx
52
app/tl.tsx
@ -3,7 +3,7 @@ import vsSource from './glsl/timeline/vert.glsl';
|
|||||||
import fsSource from './glsl/timeline/frag.glsl';
|
import fsSource from './glsl/timeline/frag.glsl';
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { formatInTimeZone } from "date-fns-tz";
|
||||||
import { parse } from "date-fns"
|
import { parse } from "date-fns"
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
timelineConfig,
|
timelineConfig,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
const { isPlaying, togglePlay, currentDatetime, setTime } = useTimeline({})
|
const { isPlaying, togglePlay, currentDatetime, setTime, setTimelineTime, timelineDatetime } = useTimeline({})
|
||||||
const [lock, setLock] = useState(false)
|
const [lock, setLock] = useState(false)
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
const ticksCanvasRef = 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) {
|
if (!lock && data.statusUpdates.newestDt) {
|
||||||
const newDt = parse(data.statusUpdates.newestDt + 'Z', 'yyyyMMddHHmmssX', new Date())
|
const newDt = parse(data.statusUpdates.newestDt + 'Z', 'yyyyMMddHHmmssX', new Date())
|
||||||
setTime(newDt)
|
setTime(newDt)
|
||||||
|
setTimelineTime(newDt)
|
||||||
|
|
||||||
|
if (timelineEngineRef.current) {
|
||||||
|
timelineEngineRef.current.replaceTimeMarkByTimestamp(newDt.getTime())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [data, lock])
|
}, [data, lock])
|
||||||
|
|
||||||
|
|
||||||
// 定时器效果 - 当播放时每隔指定时间执行操作
|
// 定时器效果 - 当播放时每隔指定时间执行操作
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let intervalId: NodeJS.Timeout | null = null;
|
let intervalId: NodeJS.Timeout | null = null;
|
||||||
@ -127,12 +133,6 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
};
|
};
|
||||||
}, [isPlaying, timeStep]);
|
}, [isPlaying, timeStep]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentDatetime && !lock) {
|
|
||||||
timelineEngineRef.current?.replaceTimeMarkByTimestamp(currentDatetime.getTime())
|
|
||||||
}
|
|
||||||
}, [currentDatetime, lock])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ticksCanvasRef.current) return;
|
if (!ticksCanvasRef.current) return;
|
||||||
|
|
||||||
@ -171,11 +171,11 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
const datestr = formatInTimeZone(date, 'UTC', 'yyyyMMddHHmmss')
|
const datestr = formatInTimeZone(date, 'UTC', 'yyyyMMddHHmmss')
|
||||||
const response = await fetch(`http://localhost:3050/api/v1/data/nearest?datetime=${datestr}&area=cn`)
|
const response = await fetch(`http://localhost:3050/api/v1/data/nearest?datetime=${datestr}&area=cn`)
|
||||||
|
|
||||||
|
setTimelineTime(date)
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
const nearestDatetime = data.nearest_data_time
|
const nearestDatetime = data.nearest_data_time
|
||||||
const nearestDate = new Date(Date.parse(nearestDatetime))
|
const nearestDate = new Date(Date.parse(nearestDatetime))
|
||||||
|
|
||||||
setTime(nearestDate)
|
setTime(nearestDate)
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to fetch data:', response.status)
|
console.error('Failed to fetch data:', response.status)
|
||||||
@ -242,6 +242,26 @@ export const Timeline: React.FC<Props> = React.memo(({
|
|||||||
}
|
}
|
||||||
}, [timeStep]);
|
}, [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 (
|
return (
|
||||||
<div className={cn(props.className, "w-full h-10 flex flex-row")}>
|
<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)' }}>
|
<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' }}
|
style={{ fontSize: '10px' }}
|
||||||
>
|
>
|
||||||
<option value="60000">1分钟</option>
|
<option value="60000">1分钟</option>
|
||||||
<option value="300000">5分钟</option>
|
|
||||||
<option value="360000">6分钟</option>
|
<option value="360000">6分钟</option>
|
||||||
<option value="600000">10分钟</option>
|
<option value="600000">10分钟</option>
|
||||||
<option value="900000">15分钟</option>
|
|
||||||
<option value="1800000">30分钟</option>
|
<option value="1800000">30分钟</option>
|
||||||
<option value="3600000">1小时</option>
|
<option value="3600000">1小时</option>
|
||||||
<option value="7200000">2小时</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} />}
|
{lock ? <LockIcon size={10} /> : <UnlockIcon size={10} />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="size-5"
|
||||||
|
onClick={() => {
|
||||||
|
handleRefresh()
|
||||||
|
}}
|
||||||
|
title="刷新"
|
||||||
|
>
|
||||||
|
<RefreshCwIcon size={10} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -83,49 +83,76 @@ export function MapComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const vertexSource = `#version 300 es
|
const vertexSource = `#version 300 es
|
||||||
|
|
||||||
layout(location = 0) in vec2 a_pos;
|
layout(location = 0) in vec2 a_pos;
|
||||||
layout(location = 1) in vec2 a_tex_coord;
|
layout(location = 1) in vec2 a_tex_coord;
|
||||||
|
|
||||||
${shaderData.vertexShaderPrelude}
|
${shaderData.vertexShaderPrelude}
|
||||||
${shaderData.define}
|
${shaderData.define}
|
||||||
|
|
||||||
out vec2 v_tex_coord;
|
out vec2 v_merc; // 归一化墨卡托坐标 (0..1)
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
gl_Position = projectTile(a_pos);
|
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
|
// WebGL2 fragment shader
|
||||||
const fragmentSource = `#version 300 es
|
const fragmentSource = `#version 300 es
|
||||||
precision highp float;
|
precision highp float;
|
||||||
|
|
||||||
uniform sampler2D u_tex;
|
uniform sampler2D u_tex;
|
||||||
uniform sampler2D u_lut;
|
uniform sampler2D u_lut;
|
||||||
|
|
||||||
|
in vec2 v_merc; // 来自 VS 的归一化墨卡托
|
||||||
out vec4 fragColor;
|
out vec4 fragColor;
|
||||||
in vec2 v_tex_coord;
|
|
||||||
|
|
||||||
void main() {
|
const float PI = 3.141592653589793;
|
||||||
vec4 texColor = texture(u_tex, v_tex_coord);
|
// 假设 bound = vec4(west, south, east, north) in degrees
|
||||||
// 对于灰度图,RGB通道通常相同,取红色通道作为灰度值
|
const vec4 bound = vec4(65.24686921730095, 11.90274236858339,
|
||||||
float value = texColor.r * 3.4;
|
138.85323419021077, 55.34323805611308);
|
||||||
|
|
||||||
if (value < 0.07) {
|
// mercator -> (lon, lat) in radians
|
||||||
fragColor= vec4(1.0,1.0,1.0,0.2);
|
vec2 mercatorToLonLat(vec2 merc) {
|
||||||
return;
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// normalizedValue = clamp(normalizedValue, 0.0, 1.0);
|
vec2 rad2deg(vec2 r) { return r * (180.0 / PI); }
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// 归一化墨卡托 -> 经纬度(度)
|
||||||
|
vec2 lonlat = rad2deg(mercatorToLonLat(v_merc));
|
||||||
|
|
||||||
|
// 经纬度 -> 纹理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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
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(lutColor.rgb, alpha);
|
||||||
// fragColor = vec4(1.0,1.0,1.0,0.2);
|
}
|
||||||
}`
|
`;
|
||||||
|
|
||||||
console.log(vertexSource, fragmentSource)
|
console.log(vertexSource, fragmentSource)
|
||||||
|
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export function useTimeline({
|
|||||||
const [isPlaying, setIsPlaying] = useState(autoPlay)
|
const [isPlaying, setIsPlaying] = useState(autoPlay)
|
||||||
const [speed, setSpeed] = useState<'slow' | 'normal' | 'fast'>('normal')
|
const [speed, setSpeed] = useState<'slow' | 'normal' | 'fast'>('normal')
|
||||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||||
const { setTime, currentDatetime } = useMap()
|
const { setTime, currentDatetime, setTimelineTime, timelineDatetime } = useMap()
|
||||||
|
|
||||||
const updateDate = useCallback((newDate: Date) => {
|
const updateDate = useCallback((newDate: Date) => {
|
||||||
setTime(newDate)
|
setTime(newDate)
|
||||||
@ -117,6 +117,8 @@ export function useTimeline({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
timelineDatetime,
|
||||||
|
setTimelineTime,
|
||||||
currentDatetime,
|
currentDatetime,
|
||||||
isPlaying,
|
isPlaying,
|
||||||
speed,
|
speed,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user