time
This commit is contained in:
parent
9321b88df1
commit
37f07671d2
@ -67,6 +67,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<SelectValue placeholder={isMapReady ? "Select a location" : "Loading map..."} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="usa">USA</SelectItem>
|
||||
<SelectItem value="singapore">Singapore</SelectItem>
|
||||
<SelectItem value="malaysia">Malaysia</SelectItem>
|
||||
<SelectItem value="china">China</SelectItem>
|
||||
|
||||
@ -4,6 +4,7 @@ import "./globals.css";
|
||||
import { TwentyFirstToolbar } from '@21st-extension/toolbar-next';
|
||||
import { ReactPlugin } from '@21st-extension/react';
|
||||
import { ThemeProvider } from '@/components/theme-provider';
|
||||
import { MapProvider } from "./map-context";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@ -31,12 +32,14 @@ export default function RootLayout({
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider defaultTheme="dark" storageKey="mosaic-theme">
|
||||
{children}
|
||||
<TwentyFirstToolbar
|
||||
config={{
|
||||
plugins: [ReactPlugin],
|
||||
}}
|
||||
/>
|
||||
<MapProvider>
|
||||
{children}
|
||||
<TwentyFirstToolbar
|
||||
config={{
|
||||
plugins: [ReactPlugin],
|
||||
}}
|
||||
/>
|
||||
</MapProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,18 +1,21 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useRef, useState, ReactNode } from 'react'
|
||||
import type { Map } from 'maplibre-gl'
|
||||
import Map from 'ol/Map';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
|
||||
// 定义MapContext的类型
|
||||
interface MapContextType {
|
||||
mapRef: React.RefObject<Map | null>
|
||||
layers: React.RefObject<any[]>
|
||||
mapState: MapState
|
||||
setMap: (map: Map) => void
|
||||
setMap: (map: Map, layers: any[]) => void
|
||||
flyTo: (options: { center: [number, number]; zoom: number; duration?: number }) => void
|
||||
zoomIn: () => void
|
||||
zoomOut: () => void
|
||||
zoomTo: (zoom: number) => void
|
||||
reset: () => void
|
||||
setTime: (date: Date) => void
|
||||
isMapReady: boolean
|
||||
}
|
||||
|
||||
@ -36,57 +39,85 @@ export function MapProvider({ children }: MapProviderProps) {
|
||||
zoomLevel: 11
|
||||
});
|
||||
|
||||
mapRef.current?.on('zoom', () => {
|
||||
setMapState(prevState => ({
|
||||
...prevState,
|
||||
zoomLevel: mapRef.current?.getZoom() || 11
|
||||
}));
|
||||
});
|
||||
const layersRef = useRef<any[]>([])
|
||||
|
||||
|
||||
const setMap = (map: Map, layers: any[]) => {
|
||||
// 监听视图变化事件
|
||||
const view = map.getView();
|
||||
|
||||
// 监听视图的缩放变化
|
||||
view.on('change:resolution', () => {
|
||||
setMapState(prevState => ({
|
||||
...prevState,
|
||||
zoomLevel: view.getZoom() || 11
|
||||
}));
|
||||
});
|
||||
|
||||
// 监听视图的中心点变化
|
||||
view.on('change:center', () => {
|
||||
const center = view.getCenter()
|
||||
|
||||
});
|
||||
|
||||
const setMap = (map: Map) => {
|
||||
mapRef.current = map;
|
||||
layersRef.current = layers;
|
||||
setIsMapReady(true);
|
||||
}
|
||||
|
||||
const flyTo = (options: { center: [number, number]; zoom: number; duration?: number }) => {
|
||||
if (mapRef.current) {
|
||||
mapRef.current.flyTo({
|
||||
...options,
|
||||
mapRef.current.getView().animate({
|
||||
center: fromLonLat(options.center),
|
||||
zoom: options.zoom,
|
||||
duration: options.duration || 1000
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const zoomIn = () => {
|
||||
if (mapRef.current) {
|
||||
mapRef.current.zoomIn()
|
||||
mapRef.current.getView().setZoom(mapRef.current.getView().getZoom()! + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const zoomOut = () => {
|
||||
if (mapRef.current) {
|
||||
mapRef.current.zoomOut()
|
||||
mapRef.current.getView().setZoom(mapRef.current.getView().getZoom()! - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const zoomTo = (zoom: number) => {
|
||||
if (mapRef.current) {
|
||||
mapRef.current.zoomTo(zoom)
|
||||
mapRef.current.getView().setZoom(zoom)
|
||||
}
|
||||
}
|
||||
|
||||
const setTime = (date: Date) => {
|
||||
if (mapRef.current) {
|
||||
layersRef.current.forEach(layer => {
|
||||
const source = layer.getSource()
|
||||
if (source) {
|
||||
source.updateParams({
|
||||
'TIME': date.toISOString()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const reset = () => {
|
||||
if (mapRef.current) {
|
||||
mapRef.current.flyTo({
|
||||
center: [103.851959, 1.290270],
|
||||
zoom: 11,
|
||||
duration: 1000
|
||||
})
|
||||
mapRef.current.getView().setCenter([103.851959, 1.290270])
|
||||
mapRef.current.getView().setZoom(11)
|
||||
}
|
||||
}
|
||||
|
||||
const value: MapContextType = {
|
||||
setTime,
|
||||
mapRef,
|
||||
layers: layersRef,
|
||||
mapState,
|
||||
setMap,
|
||||
flyTo,
|
||||
|
||||
109
app/page.tsx
109
app/page.tsx
@ -1,7 +1,4 @@
|
||||
'use client'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css';
|
||||
import type { Map, MapOptions } from 'maplibre-gl';
|
||||
import maplibregl from 'maplibre-gl';
|
||||
import { AppSidebar } from '@/app/app-sidebar'
|
||||
import {
|
||||
Breadcrumb,
|
||||
@ -15,26 +12,19 @@ import {
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from '@/components/ui/sidebar'
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { MapProvider } from './map-context';
|
||||
import { MapComponent } from '@/components/map-component';
|
||||
import { ThemeToggle } from '@/components/theme-toggle';
|
||||
import { Timeline } from '@/app/timeline';
|
||||
|
||||
import { Dock } from "@/app/dock"
|
||||
|
||||
import {
|
||||
Home,
|
||||
Search,
|
||||
Music,
|
||||
Heart,
|
||||
Settings,
|
||||
Plus,
|
||||
User,
|
||||
Play,
|
||||
Pause
|
||||
} from "lucide-react"
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTimeline } from '@/hooks/use-timeline';
|
||||
|
||||
|
||||
export default function Page() {
|
||||
@ -51,68 +41,45 @@ 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 currentDate = now;
|
||||
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const mapRef = useRef<Map | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || mapRef.current) return;
|
||||
|
||||
const options: MapOptions = {
|
||||
container: containerRef.current,
|
||||
style: 'https://demotiles.maplibre.org/style.json',
|
||||
center: [103.851959, 1.290270], // 新加坡
|
||||
zoom: 11,
|
||||
};
|
||||
|
||||
mapRef.current = new maplibregl.Map(options);
|
||||
mapRef.current.addControl(new maplibregl.NavigationControl(), 'top-right');
|
||||
return () => mapRef.current?.remove();
|
||||
}, []);
|
||||
const { setTime } = useTimeline()
|
||||
|
||||
return (
|
||||
<MapProvider>
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="sticky top-0 flex h-16 shrink-0 items-center gap-2 border-b bg-background px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
<div className="flex items-center gap-2 justify-between w-full">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>October 2024</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
<div className="relative h-full w-full flex flex-col">
|
||||
<MapComponent />
|
||||
<Timeline
|
||||
className={
|
||||
cn(
|
||||
"backdrop-blur-lg border shadow-lg",
|
||||
"bg-background/90 border-border",
|
||||
// "absolute bottom-0 left-1/2 -translate-x-1/2 ",
|
||||
"z-10"
|
||||
)
|
||||
}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
// currentDate={currentDate}
|
||||
onDateChange={(date) => {
|
||||
console.log('Selected date:', date);
|
||||
}}
|
||||
/>
|
||||
{/* <Dock items={items} className="absolute top-1/2 right-4 -translate-y-1/2" /> */}
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<SidebarInset>
|
||||
<header className="sticky top-0 flex h-16 shrink-0 items-center gap-2 border-b bg-background px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
<div className="flex items-center gap-2 justify-between w-full">
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbPage>October 2024</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</MapProvider>
|
||||
</header>
|
||||
<div className="relative h-full w-full flex flex-col">
|
||||
<MapComponent />
|
||||
<Timeline
|
||||
className={
|
||||
cn(
|
||||
"backdrop-blur-lg border shadow-lg",
|
||||
"bg-background/90 border-border",
|
||||
"z-10"
|
||||
)
|
||||
}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onDateChange={(date) => {
|
||||
console.log('Selected date:', date);
|
||||
setTime(date)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
||||
|
||||
394
app/timeline.tsx
394
app/timeline.tsx
@ -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, Play } from "lucide-react";
|
||||
import { ChevronLeft, ChevronRight, Pause, Play } from "lucide-react";
|
||||
|
||||
import {
|
||||
Select,
|
||||
@ -14,6 +14,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { useTimeline } from "@/hooks/use-timeline";
|
||||
|
||||
interface Uniforms {
|
||||
startTimestamp: number; // Unix 时间戳开始
|
||||
@ -138,7 +139,7 @@ export const Timeline: React.FC<Props> = ({
|
||||
onPause,
|
||||
boxSize = [4, 8],
|
||||
minZoom = 0.5,
|
||||
maxZoom = 2,
|
||||
maxZoom = 8,
|
||||
initialZoom = 1,
|
||||
vesicaData,
|
||||
dateFormat,
|
||||
@ -148,23 +149,30 @@ export const Timeline: React.FC<Props> = ({
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const ticksCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
const { isPlaying, togglePlay } = useTimeline({
|
||||
initialDate: currentDate ?? new Date(),
|
||||
onDateChange(date) {
|
||||
onDateChange?.(date);
|
||||
}
|
||||
})
|
||||
|
||||
const [state, setState] = useState<Status>({
|
||||
isDragging: false,
|
||||
isLongPress: false,
|
||||
isPanningTimeline: false,
|
||||
customLineTimestamp: null,
|
||||
customLineTimestamp: currentDate?.getTime() ?? new Date().getTime(),
|
||||
panOffset: 0,
|
||||
zoomLevel: initialZoom,
|
||||
});
|
||||
|
||||
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null); // 长按计时器
|
||||
const dragTempXRef = useRef<number | null>(null); // 拖拽时的临时x坐标
|
||||
const animationFrameRef = useRef<number | null>(null); // 动画帧ID
|
||||
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const dragTempXRef = useRef<number | null>(null);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
|
||||
const panStartXRef = useRef<number>(0); // 拖拽开始的x坐标
|
||||
const panStartOffsetRef = useRef<number>(0); // 拖拽开始时的panOffset
|
||||
const panTempOffsetRef = useRef<number>(0); // 拖拽时的临时偏移
|
||||
const hasDraggedTimelineRef = useRef<boolean>(false); // 拖拽标记,使用ref避免闪烁
|
||||
const panStartXRef = useRef<number>(0);
|
||||
const panStartOffsetRef = useRef<number>(0);
|
||||
const panTempOffsetRef = useRef<number>(0);
|
||||
const hasDraggedTimelineRef = useRef<boolean>(false);
|
||||
|
||||
const actualVesicaData = vesicaData ?? [];
|
||||
|
||||
@ -176,9 +184,16 @@ export const Timeline: React.FC<Props> = ({
|
||||
stateRef.current = state;
|
||||
propsRef.current = { startDate, endDate, currentDate };
|
||||
|
||||
const forward = useCallback(() => {
|
||||
const newDate = new Date(state.customLineTimestamp! + 360000);
|
||||
setState({ ...state, customLineTimestamp: newDate.getTime() });
|
||||
onDateChange?.(newDate);
|
||||
}, [state.customLineTimestamp])
|
||||
|
||||
// 缩放处理函数
|
||||
const handleZoom = (delta: number, mouseX?: number) => {
|
||||
const zoomFactor = 1.15; // 调整缩放因子,使缩放更平滑
|
||||
|
||||
const newZoom = delta > 0
|
||||
? Math.min(state.zoomLevel * zoomFactor, maxZoom)
|
||||
: Math.max(state.zoomLevel / zoomFactor, minZoom);
|
||||
@ -186,14 +201,43 @@ export const Timeline: React.FC<Props> = ({
|
||||
// 如果缩放级别没有变化,直接返回
|
||||
if (newZoom === state.zoomLevel) return;
|
||||
|
||||
// 可选:基于鼠标位置的缩放(保持鼠标位置下的内容不变)
|
||||
// 基于鼠标位置的缩放(保持鼠标位置下的内容不变)
|
||||
let newPanOffset = state.panOffset;
|
||||
if (mouseX !== undefined && ticksCanvasRef.current) {
|
||||
if (mouseX !== undefined && ticksCanvasRef.current && startDate && endDate) {
|
||||
const rect = ticksCanvasRef.current.getBoundingClientRect();
|
||||
const centerX = rect.width / 2;
|
||||
const mouseOffsetFromCenter = mouseX - centerX;
|
||||
const zoomChange = newZoom / state.zoomLevel;
|
||||
newPanOffset = state.panOffset + mouseOffsetFromCenter * (1 - zoomChange);
|
||||
const startX = 40; // 时间轴左边距,与drawTicks保持一致
|
||||
const endX = rect.width - 40; // 时间轴右边距
|
||||
const timelineWidth = endX - startX;
|
||||
|
||||
// 计算总时间范围
|
||||
const timeRange = endDate.getTime() - startDate.getTime();
|
||||
|
||||
// 计算当前可见时间窗口(缩放前)
|
||||
const currentVisibleTimeRange = timeRange / state.zoomLevel;
|
||||
const currentTimePerPixel = currentVisibleTimeRange / timelineWidth;
|
||||
const currentPanTimeOffset = -state.panOffset * currentTimePerPixel;
|
||||
const originalCenterTime = startDate.getTime() + timeRange / 2;
|
||||
const currentCenterTime = originalCenterTime + currentPanTimeOffset;
|
||||
const currentVisibleStartTime = currentCenterTime - currentVisibleTimeRange / 2;
|
||||
|
||||
// 计算鼠标位置对应的时间点
|
||||
const mouseRelativeX = mouseX - startX;
|
||||
const mouseTimeProgress = mouseRelativeX / timelineWidth;
|
||||
const mouseTime = currentVisibleStartTime + mouseTimeProgress * currentVisibleTimeRange;
|
||||
|
||||
// 计算缩放后的可见时间窗口
|
||||
const newVisibleTimeRange = timeRange / newZoom;
|
||||
const newTimePerPixel = newVisibleTimeRange / timelineWidth;
|
||||
|
||||
// 计算新的中心时间,使鼠标位置对应的时间点保持不变
|
||||
const newVisibleStartTime = mouseTime - mouseTimeProgress * newVisibleTimeRange;
|
||||
const newCenterTime = newVisibleStartTime + newVisibleTimeRange / 2;
|
||||
const newPanTimeOffset = newCenterTime - originalCenterTime;
|
||||
newPanOffset = -newPanTimeOffset / newTimePerPixel;
|
||||
|
||||
// 应用边界限制
|
||||
const maxPanOffset = 100 * newZoom;
|
||||
newPanOffset = Math.max(-maxPanOffset, Math.min(maxPanOffset, newPanOffset));
|
||||
}
|
||||
|
||||
setState({ ...state, zoomLevel: newZoom, panOffset: newPanOffset });
|
||||
@ -204,6 +248,7 @@ export const Timeline: React.FC<Props> = ({
|
||||
// 计算平移边界限制
|
||||
const maxPanOffset = 100 * state.zoomLevel; // 最大平移距离与缩放级别相关
|
||||
const newOffset = Math.max(-maxPanOffset, Math.min(maxPanOffset, state.panOffset + deltaX));
|
||||
// const newOffset = state.panOffset + deltaX
|
||||
setState({ ...state, panOffset: newOffset });
|
||||
};
|
||||
|
||||
@ -251,11 +296,17 @@ export const Timeline: React.FC<Props> = ({
|
||||
const currentState = stateRef.current;
|
||||
if ((currentState.isDragging && currentState.isLongPress) || currentState.isPanningTimeline) return;
|
||||
|
||||
redraw();
|
||||
// 使用requestAnimationFrame优化重绘性能
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
redraw();
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(rafId);
|
||||
};
|
||||
}, [state.customLineTimestamp, state.zoomLevel, state.panOffset, startDate, endDate, currentDate, redraw]);
|
||||
|
||||
const { radius, d } = calcVersicaUni(boxSize);
|
||||
console.log(radius, d);
|
||||
|
||||
const current_uniforms = useRef<Uniforms>({
|
||||
startTimestamp: 0,
|
||||
@ -437,35 +488,29 @@ export const Timeline: React.FC<Props> = ({
|
||||
}
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(() => {
|
||||
if (ticksCanvasRef.current && dragTempXRef.current !== null) {
|
||||
// 直接重绘,使用临时位置
|
||||
// 将屏幕坐标转换为时间戳
|
||||
if (startDate && endDate && dragTempXRef.current !== null) {
|
||||
const rect = ticksCanvasRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
const timelineStartX = 40;
|
||||
const timelineEndX = rect.width - 40;
|
||||
const timelineWidth = timelineEndX - timelineStartX;
|
||||
const relativeX = dragTempXRef.current - timelineStartX;
|
||||
const progress = relativeX / timelineWidth;
|
||||
if (ticksCanvasRef.current && dragTempXRef.current !== null && startDate && endDate) {
|
||||
const rect = ticksCanvasRef.current.getBoundingClientRect();
|
||||
const timelineStartX = 40;
|
||||
const timelineEndX = rect.width - 40;
|
||||
const timelineWidth = timelineEndX - timelineStartX;
|
||||
const relativeX = dragTempXRef.current - timelineStartX;
|
||||
const progress = relativeX / timelineWidth;
|
||||
|
||||
// 基于可见时间窗口计算时间戳
|
||||
const timeRange = endDate.getTime() - startDate.getTime();
|
||||
const visibleTimeRange = timeRange / currentState.zoomLevel;
|
||||
const timePerPixel = visibleTimeRange / timelineWidth;
|
||||
const panTimeOffset = -currentState.panOffset * timePerPixel;
|
||||
const originalCenterTime = startDate.getTime() + timeRange / 2;
|
||||
const newCenterTime = originalCenterTime + panTimeOffset;
|
||||
const visibleStartTime = newCenterTime - visibleTimeRange / 2;
|
||||
// 基于可见时间窗口计算时间戳
|
||||
const timeRange = endDate.getTime() - startDate.getTime();
|
||||
const visibleTimeRange = timeRange / currentState.zoomLevel;
|
||||
const timePerPixel = visibleTimeRange / timelineWidth;
|
||||
const panTimeOffset = -currentState.panOffset * timePerPixel;
|
||||
const originalCenterTime = startDate.getTime() + timeRange / 2;
|
||||
const newCenterTime = originalCenterTime + panTimeOffset;
|
||||
const visibleStartTime = newCenterTime - visibleTimeRange / 2;
|
||||
|
||||
const dragTimestamp = visibleStartTime + progress * visibleTimeRange;
|
||||
const dragTimestamp = visibleStartTime + progress * visibleTimeRange;
|
||||
|
||||
redraw({
|
||||
customLineTimestamp: dragTimestamp,
|
||||
isDragging: true
|
||||
});
|
||||
}
|
||||
}
|
||||
redraw({
|
||||
customLineTimestamp: dragTimestamp,
|
||||
isDragging: true
|
||||
});
|
||||
}
|
||||
});
|
||||
return;
|
||||
@ -720,7 +765,6 @@ export const Timeline: React.FC<Props> = ({
|
||||
// 绑定uniform buffer
|
||||
gl.bindBufferBase(gl.UNIFORM_BUFFER, 0, uniform_bfRef.current);
|
||||
|
||||
console.log(`绘制实例数量: ${instants_countRef.current}`);
|
||||
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instants_countRef.current);
|
||||
gl.bindVertexArray(null);
|
||||
}
|
||||
@ -1008,11 +1052,11 @@ export const Timeline: React.FC<Props> = ({
|
||||
<ChevronLeft size={10} />
|
||||
</Button>
|
||||
|
||||
<Button variant="secondary" size="icon" className="size-5">
|
||||
<Play size={10} />
|
||||
<Button variant="secondary" size="icon" className="size-5" onClick={() => togglePlay()}>
|
||||
{isPlaying ? <Pause size={10} /> : <Play size={10} />}
|
||||
</Button>
|
||||
|
||||
<Button variant="secondary" size="icon" className="size-5">
|
||||
<Button variant="secondary" size="icon" className="size-5" onClick={() => forward()}>
|
||||
<ChevronRight size={10} />
|
||||
</Button>
|
||||
|
||||
@ -1171,8 +1215,6 @@ function createVesicaInstances(
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`生成vesica实例 (DPR=${dpr}): 数量=${instants.length}`);
|
||||
|
||||
const instants_bf = gl.createBuffer()!;
|
||||
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, instants_bf);
|
||||
@ -1198,10 +1240,6 @@ function calcVersicaUni(box_size: [number, number]) {
|
||||
const radius = Math.max(w * 0.6, h * 0.6); // 确保能包含形状
|
||||
const d = Math.min(w * 0.4, radius * 1.5); // 确保 d < 2*radius
|
||||
|
||||
console.log(`vesica参数: w=${w}, h=${h}, radius=${radius.toFixed(2)}, d=${d.toFixed(2)}`);
|
||||
console.log(`验证: d < 2*radius? ${d} < ${2 * radius} = ${d < 2 * radius}`);
|
||||
console.log(`验证: r²-d² = ${radius * radius} - ${d * d} = ${radius * radius - d * d}`);
|
||||
|
||||
return {
|
||||
radius,
|
||||
d,
|
||||
@ -1223,6 +1261,48 @@ function setupTicksCanvas(canvas: HTMLCanvasElement, width: number, height: numb
|
||||
// 这里我们不需要手动触发,因为canvas尺寸变化会自然触发重绘
|
||||
}
|
||||
|
||||
|
||||
|
||||
function levelInfo(level: number) {
|
||||
// 根据缩放级别确定时间轴级别
|
||||
if (level >= 6.0) {
|
||||
// 日视角:主刻度为小时,子刻度为分钟
|
||||
return {
|
||||
level: 'day',
|
||||
majorStep: 3600000, // 1小时 = 3600000ms
|
||||
minorStep: 600000, // 10分钟 = 600000ms,在1小时内有6个子刻度
|
||||
majorTickHeight: 8,
|
||||
minorTickHeight: 4,
|
||||
showMinorTicks: true,
|
||||
majorLabelFormat: (date: Date) => date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit'
|
||||
}),
|
||||
minorLabelFormat: (date: Date) => date.toLocaleTimeString('zh-CN', {
|
||||
minute: '2-digit'
|
||||
})
|
||||
};
|
||||
} else {
|
||||
// 月视角:主刻度为日期,子刻度为小时
|
||||
return {
|
||||
level: 'month',
|
||||
majorStep: 86400000, // 1天 = 86400000ms
|
||||
minorStep: 7200000, // 2小时 = 7200000ms
|
||||
majorTickHeight: 7,
|
||||
minorTickHeight: 3,
|
||||
showMinorTicks: level > 1.2, // 缩放级别大于1.2时显示子刻度
|
||||
majorLabelFormat: (date: Date) => date.toLocaleDateString('zh-CN', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}),
|
||||
minorLabelFormat: (date: Date) => date.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit'
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function drawTicks(
|
||||
canvas: HTMLCanvasElement,
|
||||
width: number,
|
||||
@ -1256,45 +1336,23 @@ function drawTicks(
|
||||
const endX = width - 40; // 右边距
|
||||
const timelineWidth = endX - startX;
|
||||
|
||||
// 根据缩放级别计算刻度间距
|
||||
const baseTickInterval = 60; // 基础刻度间距
|
||||
const scaledTickInterval = baseTickInterval * zoomLevel;
|
||||
|
||||
// 主刻度参数
|
||||
const majorTickHeight = 7;
|
||||
const majorTickInterval = scaledTickInterval;
|
||||
|
||||
// 子刻度参数 - 只在缩放级别足够大时显示
|
||||
const minorTickHeight = 3;
|
||||
const minorTickInterval = scaledTickInterval / 5; // 每个主刻度之间5个子刻度
|
||||
const showMinorTicks = zoomLevel > 1.0 && minorTickInterval > 8; // 缩放级别大于1.0且间距足够大时显示子刻度
|
||||
// 获取当前级别信息
|
||||
const levelConfig = levelInfo(zoomLevel);
|
||||
const {
|
||||
level,
|
||||
majorStep,
|
||||
minorStep,
|
||||
majorTickHeight,
|
||||
minorTickHeight,
|
||||
showMinorTicks,
|
||||
majorLabelFormat,
|
||||
minorLabelFormat
|
||||
} = levelConfig;
|
||||
|
||||
// 计算时间范围
|
||||
let timeRange = 0;
|
||||
let majorTimeStep = 0;
|
||||
|
||||
if (startDate && endDate) {
|
||||
timeRange = endDate.getTime() - startDate.getTime();
|
||||
|
||||
// 根据当前可见时间范围确定合适的时间步长
|
||||
// 考虑缩放级别,但时间步长本身保持离散的固定值
|
||||
const visibleTimeRange = timeRange / zoomLevel;
|
||||
|
||||
if (visibleTimeRange < 1800000) { // 可见范围小于30分钟
|
||||
majorTimeStep = 300000; // 5分钟间隔
|
||||
} else if (visibleTimeRange < 7200000) { // 可见范围小于2小时
|
||||
majorTimeStep = 900000; // 15分钟间隔
|
||||
} else if (visibleTimeRange < 43200000) { // 可见范围小于12小时
|
||||
majorTimeStep = 3600000; // 1小时间隔
|
||||
} else if (visibleTimeRange < 259200000) { // 可见范围小于3天
|
||||
majorTimeStep = 21600000; // 6小时间隔
|
||||
} else if (visibleTimeRange < 1209600000) { // 可见范围小于2周
|
||||
majorTimeStep = 86400000; // 1天间隔
|
||||
} else if (visibleTimeRange < 5184000000) { // 可见范围小于2个月
|
||||
majorTimeStep = 604800000; // 1周间隔
|
||||
} else { // 可见范围超过2个月
|
||||
majorTimeStep = 2592000000; // 1个月间隔
|
||||
}
|
||||
}
|
||||
|
||||
// 计算可见时间窗口(考虑缩放和平移)
|
||||
@ -1318,36 +1376,6 @@ function drawTicks(
|
||||
visibleEndTime = newCenterTime + visibleTimeRange / 2;
|
||||
}
|
||||
|
||||
// 绘制主时间轴线 - 根据实际刻度范围动态调整长度
|
||||
// ctx.strokeStyle = '#888';
|
||||
// ctx.lineWidth = 2;
|
||||
// ctx.beginPath();
|
||||
|
||||
// 计算实际需要覆盖的范围
|
||||
// let lineStartX = startX;
|
||||
// let lineEndX = endX;
|
||||
|
||||
// // 如果有时间数据,根据可见刻度范围扩展线条
|
||||
// if (startDate && endDate) {
|
||||
// const startTickTime = Math.floor(visibleStartTime / majorTimeStep) * majorTimeStep;
|
||||
// const endTickTime = visibleEndTime + majorTimeStep;
|
||||
|
||||
// // 计算第一个和最后一个刻度的屏幕位置
|
||||
// const firstTickProgress = (startTickTime - visibleStartTime) / (visibleEndTime - visibleStartTime);
|
||||
// const lastTickProgress = (endTickTime - visibleStartTime) / (visibleEndTime - visibleStartTime);
|
||||
|
||||
// const firstTickX = startX + firstTickProgress * timelineWidth;
|
||||
// const lastTickX = startX + lastTickProgress * timelineWidth;
|
||||
|
||||
// // 扩展线条以覆盖所有刻度,但限制在合理范围内
|
||||
// lineStartX = Math.max(0, Math.min(startX, firstTickX - 20));
|
||||
// lineEndX = Math.min(width, Math.max(endX, lastTickX + 20));
|
||||
// }
|
||||
|
||||
// ctx.moveTo(lineStartX, centerY);
|
||||
// ctx.lineTo(lineEndX, centerY);
|
||||
// ctx.stroke();
|
||||
|
||||
ctx.fillStyle = '#111';
|
||||
ctx.fillRect(0, 0, width, 16);
|
||||
|
||||
@ -1381,27 +1409,9 @@ function drawTicks(
|
||||
ctx.fillStyle = isDragging ? '#ff6666' : '#ff4444';
|
||||
ctx.font = 'bold 10px Arial';
|
||||
ctx.textBaseline = 'bottom';
|
||||
let label = '';
|
||||
const tickTime = new Date(customLineTimestamp);
|
||||
|
||||
if (majorTimeStep < 86400000) { // 小于1天,显示时间
|
||||
label = tickTime.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} else if (majorTimeStep < 2592000000) { // 小于30天,显示日期
|
||||
label = tickTime.toLocaleDateString('zh-CN', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} else { // 大于30天,显示月份
|
||||
label = tickTime.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'short'
|
||||
});
|
||||
}
|
||||
|
||||
// ctx.fillText(label || '自定义位置', customLineX, centerY + 22);
|
||||
const label = majorLabelFormat(tickTime);
|
||||
ctx.fillText(label, customLineX, centerY - 22);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1410,29 +1420,48 @@ function drawTicks(
|
||||
ctx.strokeStyle = '#ccc';
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
const minorTimeStep = majorTimeStep / 5;
|
||||
const startTickTime = Math.floor(visibleStartTime / minorTimeStep) * minorTimeStep;
|
||||
const endTickTime = visibleEndTime + minorTimeStep;
|
||||
// 更精确的子刻度时间范围计算
|
||||
const startTickTime = Math.floor(visibleStartTime / minorStep) * minorStep;
|
||||
const endTickTime = Math.ceil(visibleEndTime / minorStep) * minorStep;
|
||||
|
||||
for (let tickTime = startTickTime; tickTime <= endTickTime; tickTime += minorTimeStep) {
|
||||
// 根据缩放级别动态调整子刻度间距
|
||||
const minMinorTickSpacing = zoomLevel >= 8 ? 6 : zoomLevel >= 6 ? 8 : 12;
|
||||
|
||||
let lastMinorTickX = -minMinorTickSpacing;
|
||||
let minorTickCount = 0; // 添加计数器用于调试
|
||||
|
||||
for (let tickTime = startTickTime; tickTime <= endTickTime; tickTime += minorStep) {
|
||||
// 计算这个时间点在屏幕上的位置
|
||||
const timeProgress = (tickTime - visibleStartTime) / (visibleEndTime - visibleStartTime);
|
||||
const x = startX + timeProgress * timelineWidth;
|
||||
|
||||
// 只绘制在可见区域内的刻度
|
||||
if (x < startX - 10 || x > endX + 10) continue;
|
||||
// 只绘制在可见区域内的刻度,扩大边界以避免边缘漏画
|
||||
if (x < startX - 20 || x > endX + 20) continue;
|
||||
|
||||
// 跳过主刻度位置
|
||||
const timeFromStart = tickTime - visibleStartTime;
|
||||
if (Math.abs(timeFromStart % majorTimeStep) < minorTimeStep * 0.1) continue;
|
||||
// 跳过主刻度位置 - 使用容差来处理浮点数精度问题
|
||||
const timeDiffFromMajor = tickTime % majorStep;
|
||||
const tolerance = 1000; // 减小容差到1秒,避免误判
|
||||
const isAtMajorTick = Math.abs(timeDiffFromMajor) < tolerance ||
|
||||
Math.abs(timeDiffFromMajor - majorStep) < tolerance;
|
||||
|
||||
// 绘制子刻度线
|
||||
ctx.beginPath();
|
||||
// ctx.moveTo(x, centerY - minorTickHeight / 2);
|
||||
// ctx.lineTo(x, centerY + minorTickHeight / 2);
|
||||
ctx.moveTo(x, 16 - minorTickHeight);
|
||||
ctx.lineTo(x, 16);
|
||||
ctx.stroke();
|
||||
if (isAtMajorTick) continue;
|
||||
|
||||
// 检查子刻度间距,避免过于密集
|
||||
const spacingFromLast = x - lastMinorTickX;
|
||||
if (spacingFromLast >= minMinorTickSpacing) {
|
||||
// 绘制子刻度线
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 16 - minorTickHeight);
|
||||
ctx.lineTo(x, 16);
|
||||
ctx.stroke();
|
||||
lastMinorTickX = x;
|
||||
minorTickCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 仅在调试模式下输出错误信息
|
||||
if (minorTickCount === 0 && showMinorTicks && zoomLevel >= 6) {
|
||||
console.warn(`子刻度绘制异常: 缩放级别${zoomLevel.toFixed(1)}, 未绘制任何子刻度`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1446,10 +1475,14 @@ function drawTicks(
|
||||
ctx.textBaseline = 'top';
|
||||
|
||||
// 基于可见时间窗口计算刻度
|
||||
const startTickTime = Math.floor(visibleStartTime / majorTimeStep) * majorTimeStep;
|
||||
const endTickTime = visibleEndTime + majorTimeStep;
|
||||
const startTickTime = Math.floor(visibleStartTime / majorStep) * majorStep;
|
||||
const endTickTime = visibleEndTime + majorStep;
|
||||
|
||||
for (let tickTime = startTickTime; tickTime <= endTickTime; tickTime += majorTimeStep) {
|
||||
// 计算文字间距,避免重叠
|
||||
const minLabelSpacing = 60; // 最小标签间距(像素)
|
||||
let lastLabelX = -minLabelSpacing; // 上一个标签的位置
|
||||
|
||||
for (let tickTime = startTickTime; tickTime <= endTickTime; tickTime += majorStep) {
|
||||
// 计算这个时间点在屏幕上的位置
|
||||
const timeProgress = (tickTime - visibleStartTime) / (visibleEndTime - visibleStartTime);
|
||||
const x = startX + timeProgress * timelineWidth;
|
||||
@ -1459,35 +1492,18 @@ function drawTicks(
|
||||
|
||||
// 绘制主刻度线
|
||||
ctx.beginPath();
|
||||
// ctx.moveTo(x, centerY - majorTickHeight / 2);
|
||||
// ctx.lineTo(x, centerY + majorTickHeight / 2);
|
||||
ctx.moveTo(x, 16 - majorTickHeight);
|
||||
ctx.lineTo(x, 16);
|
||||
ctx.stroke();
|
||||
|
||||
// 绘制时间标签
|
||||
let label = '';
|
||||
const tickDate = new Date(tickTime);
|
||||
|
||||
// 根据时间间隔选择合适的显示格式
|
||||
if (majorTimeStep < 86400000) { // 小于1天,显示时间
|
||||
label = tickDate.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} else if (majorTimeStep < 2592000000) { // 小于30天,显示日期
|
||||
label = tickDate.toLocaleDateString('zh-CN', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} else { // 大于30天,显示月份
|
||||
label = tickDate.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'short'
|
||||
});
|
||||
// 检查文字间距,避免重叠
|
||||
if (x - lastLabelX >= minLabelSpacing) {
|
||||
// 绘制时间标签
|
||||
const tickDate = new Date(tickTime);
|
||||
const label = majorLabelFormat(tickDate);
|
||||
ctx.fillText(label, x, 16 - majorTickHeight / 2 + 4);
|
||||
lastLabelX = x;
|
||||
}
|
||||
|
||||
ctx.fillText(label, x, 16 - majorTickHeight / 2 + 4);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1522,24 +1538,10 @@ function drawTicks(
|
||||
ctx.fillStyle = '#ff4444';
|
||||
ctx.font = 'bold 10px Arial';
|
||||
ctx.textBaseline = 'bottom';
|
||||
const currentLabel = currentDate.toLocaleDateString('zh-CN', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
const currentLabel = majorLabelFormat(currentDate);
|
||||
ctx.fillText(currentLabel, currentX, centerY - 22);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制缩放级别指示器(调试用)
|
||||
// ctx.fillStyle = '#888';
|
||||
// ctx.font = '10px Arial';
|
||||
// ctx.textAlign = 'left';
|
||||
// ctx.textBaseline = 'top';
|
||||
// ctx.fillText(`缩放: ${zoomLevel.toFixed(2)}x`, 10, 10);
|
||||
// ctx.fillText(`子刻度: ${showMinorTicks ? '显示' : '隐藏'} (间距: ${minorTickInterval.toFixed(1)})`, 10, 25);
|
||||
|
||||
// 恢复context状态
|
||||
ctx.restore();
|
||||
}
|
||||
@ -2,9 +2,20 @@
|
||||
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import maplibregl from 'maplibre-gl'
|
||||
import VectorTileLayer from 'ol/layer/VectorTile.js'
|
||||
import 'maplibre-gl/dist/maplibre-gl.css'
|
||||
import { useMap } from '@/app/map-context'
|
||||
import { apply, applyStyle } from 'ol-mapbox-style';
|
||||
import { useTheme } from '@/components/theme-provider'
|
||||
import TileWMS from 'ol/source/TileWMS.js';
|
||||
import Map from 'ol/Map';
|
||||
import View from 'ol/View';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
import { transformExtent, fromLonLat } from 'ol/proj.js';
|
||||
import StadiaMaps from 'ol/source/StadiaMaps.js';
|
||||
import XYZ from 'ol/source/XYZ';
|
||||
import 'ol/ol.css';
|
||||
import { useMapLocation } from '@/hooks/use-map-location'
|
||||
|
||||
interface MapComponentProps {
|
||||
style?: string
|
||||
@ -12,30 +23,52 @@ interface MapComponentProps {
|
||||
zoom?: number
|
||||
}
|
||||
|
||||
const interval = 3 * 60 * 60 * 1000;
|
||||
const step = 15 * 60 * 1000;
|
||||
const frameRate = 0.5; // frames per second
|
||||
const extent = transformExtent([-126, 24, -66, 50], 'EPSG:4326', 'EPSG:3857');
|
||||
|
||||
export function MapComponent({
|
||||
style = 'https://api.maptiler.com/maps/019817f1-82a8-7f37-901d-4bedf68b27fb/style.json?key=hj3fxRdwF9KjEsBq8sYI',
|
||||
center = [103.851959, 1.290270],
|
||||
zoom = 11
|
||||
// center = [103.851959, 1.290270],
|
||||
// zoom = 11
|
||||
}: MapComponentProps) {
|
||||
const mapContainer = useRef<HTMLDivElement>(null)
|
||||
const { setMap } = useMap()
|
||||
const { location } = useMapLocation()
|
||||
|
||||
useEffect(() => {
|
||||
if (!mapContainer.current) return
|
||||
|
||||
debugger
|
||||
const map = new maplibregl.Map({
|
||||
container: mapContainer.current,
|
||||
style,
|
||||
center,
|
||||
zoom
|
||||
const tileWmsLayer = new TileLayer({
|
||||
extent: extent,
|
||||
source: new TileWMS({
|
||||
attributions: ['Iowa State University'],
|
||||
url: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi',
|
||||
params: { 'LAYERS': 'nexrad-n0r-wmst' },
|
||||
}),
|
||||
opacity: 0.7,
|
||||
});
|
||||
|
||||
const map = new Map({
|
||||
target: mapContainer.current,
|
||||
view: new View({
|
||||
center: fromLonLat(location.center),
|
||||
zoom: location.zoom,
|
||||
projection: 'EPSG:3857',
|
||||
|
||||
showFullExtent: true,
|
||||
enableRotation: true
|
||||
|
||||
}),
|
||||
})
|
||||
|
||||
setMap(map)
|
||||
apply(map, style).then(() => {
|
||||
map.addLayer(tileWmsLayer)
|
||||
})
|
||||
|
||||
setMap(map, [tileWmsLayer])
|
||||
|
||||
return () => {
|
||||
map.remove()
|
||||
}
|
||||
}, [mapContainer])
|
||||
|
||||
return (
|
||||
|
||||
@ -10,7 +10,7 @@ export function MapExample() {
|
||||
<MapProvider>
|
||||
<div className="relative w-full h-screen">
|
||||
<MapComponent
|
||||
center={[116.4074, 39.9042]} // 北京坐标
|
||||
center={[103.851959, 1.290270]} // 北京坐标
|
||||
zoom={10}
|
||||
/>
|
||||
<MapControls />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import React, { useCallback } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
@ -14,6 +14,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import { format, addDays, subDays, startOfDay } from 'date-fns'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useTimeline } from '@/hooks/use-timeline'
|
||||
|
||||
interface TimelineProps {
|
||||
className?: string
|
||||
@ -44,6 +45,24 @@ export function Timeline({
|
||||
const totalDays = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
|
||||
const currentDays = Math.ceil((currentDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24))
|
||||
const progress = Math.max(0, Math.min(100, (currentDays / totalDays) * 100))
|
||||
const timeline = useTimeline(
|
||||
{
|
||||
onDateChange(date) {
|
||||
console.log(date)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const forward = useCallback(() => {
|
||||
}, [timeline])
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
// setFrameIndex((prev) => (prev + 1) % 10); // 每隔3秒切换一帧
|
||||
}, 60000);
|
||||
|
||||
return () => clearInterval(timer); // 清除定时器
|
||||
}, []);
|
||||
|
||||
const handleSliderChange = useCallback((value: number[]) => {
|
||||
const newProgress = value[0]
|
||||
|
||||
@ -2,6 +2,10 @@ import { useState, useCallback } from 'react'
|
||||
import { useMap } from '@/app/map-context'
|
||||
|
||||
const LOCATIONS = {
|
||||
usa: {
|
||||
center: [-95.7129, 37.0902] as [number, number],
|
||||
zoom: 4
|
||||
},
|
||||
singapore: {
|
||||
center: [103.851959, 1.290270] as [number, number],
|
||||
zoom: 11
|
||||
@ -20,7 +24,7 @@ const LOCATIONS = {
|
||||
export type LocationKey = keyof typeof LOCATIONS
|
||||
|
||||
export function useMapLocation() {
|
||||
const [currentLocation, setCurrentLocation] = useState<LocationKey>('singapore')
|
||||
const [currentLocation, setCurrentLocation] = useState<LocationKey>('usa')
|
||||
const { flyTo, isMapReady } = useMap()
|
||||
|
||||
const flyToLocation = useCallback((location: LocationKey) => {
|
||||
@ -42,8 +46,11 @@ export function useMapLocation() {
|
||||
})
|
||||
}, [flyTo])
|
||||
|
||||
const location = LOCATIONS[currentLocation]
|
||||
|
||||
return {
|
||||
currentLocation,
|
||||
location,
|
||||
flyToLocation,
|
||||
flyToCustomLocation,
|
||||
locations: LOCATIONS,
|
||||
|
||||
121
hooks/use-map-time.ts
Normal file
121
hooks/use-map-time.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { addDays, subDays } from 'date-fns'
|
||||
|
||||
interface UseTimelineOptions {
|
||||
startDate?: Date
|
||||
endDate?: Date
|
||||
initialDate?: Date
|
||||
onDateChange?: (date: Date) => void
|
||||
autoPlay?: boolean
|
||||
}
|
||||
|
||||
export function useTimeline({
|
||||
startDate = subDays(new Date(), 30),
|
||||
endDate = new Date(),
|
||||
initialDate = new Date(),
|
||||
onDateChange,
|
||||
autoPlay = false
|
||||
}: UseTimelineOptions = {}) {
|
||||
const [currentDate, setCurrentDate] = useState(initialDate)
|
||||
const [isPlaying, setIsPlaying] = useState(autoPlay)
|
||||
const [speed, setSpeed] = useState<'slow' | 'normal' | 'fast'>('normal')
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const speedIntervals = {
|
||||
slow: 2000,
|
||||
normal: 1000,
|
||||
fast: 500
|
||||
}
|
||||
|
||||
const updateDate = useCallback((newDate: Date) => {
|
||||
setCurrentDate(newDate)
|
||||
onDateChange?.(newDate)
|
||||
}, [onDateChange])
|
||||
|
||||
const play = useCallback(() => {
|
||||
setIsPlaying(true)
|
||||
}, [])
|
||||
|
||||
const pause = useCallback(() => {
|
||||
setIsPlaying(false)
|
||||
}, [])
|
||||
|
||||
const togglePlay = useCallback(() => {
|
||||
setIsPlaying(prev => !prev)
|
||||
}, [])
|
||||
|
||||
const skipForward = useCallback(() => {
|
||||
const newDate = addDays(currentDate, 1)
|
||||
if (newDate <= endDate) {
|
||||
updateDate(newDate)
|
||||
}
|
||||
}, [currentDate, endDate, updateDate])
|
||||
|
||||
const skipBackward = useCallback(() => {
|
||||
const newDate = subDays(currentDate, 1)
|
||||
if (newDate >= startDate) {
|
||||
updateDate(newDate)
|
||||
}
|
||||
}, [currentDate, startDate, updateDate])
|
||||
|
||||
const changeSpeed = useCallback((newSpeed: 'slow' | 'normal' | 'fast') => {
|
||||
setSpeed(newSpeed)
|
||||
}, [])
|
||||
|
||||
const jumpToDate = useCallback((date: Date) => {
|
||||
if (date >= startDate && date <= endDate) {
|
||||
updateDate(date)
|
||||
}
|
||||
}, [startDate, endDate, updateDate])
|
||||
|
||||
// 自动播放逻辑
|
||||
useEffect(() => {
|
||||
if (isPlaying) {
|
||||
intervalRef.current = setInterval(() => {
|
||||
const nextDate = addDays(currentDate, 1)
|
||||
if (nextDate <= endDate) {
|
||||
updateDate(nextDate)
|
||||
} else {
|
||||
// 到达结束日期,停止播放
|
||||
setIsPlaying(false)
|
||||
}
|
||||
}, speedIntervals[speed])
|
||||
} else {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
}
|
||||
}, [isPlaying, currentDate, endDate, speed, updateDate])
|
||||
|
||||
// 清理定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
currentDate,
|
||||
isPlaying,
|
||||
speed,
|
||||
startDate,
|
||||
endDate,
|
||||
play,
|
||||
pause,
|
||||
togglePlay,
|
||||
skipForward,
|
||||
skipBackward,
|
||||
changeSpeed,
|
||||
jumpToDate,
|
||||
updateDate
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { addDays, subDays } from 'date-fns'
|
||||
import { useMap } from '@/app/map-context'
|
||||
|
||||
interface UseTimelineOptions {
|
||||
startDate?: Date
|
||||
@ -21,6 +22,8 @@ export function useTimeline({
|
||||
const [speed, setSpeed] = useState<'slow' | 'normal' | 'fast'>('normal')
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const { setTime } = useMap()
|
||||
|
||||
const speedIntervals = {
|
||||
slow: 2000,
|
||||
normal: 1000,
|
||||
@ -116,6 +119,7 @@ export function useTimeline({
|
||||
skipBackward,
|
||||
changeSpeed,
|
||||
jumpToDate,
|
||||
updateDate
|
||||
updateDate,
|
||||
setTime
|
||||
}
|
||||
}
|
||||
227
package-lock.json
generated
227
package-lock.json
generated
@ -29,6 +29,8 @@
|
||||
"lucide-react": "^0.525.0",
|
||||
"maplibre-gl": "^5.6.1",
|
||||
"next": "15.4.1",
|
||||
"ol": "^10.6.1",
|
||||
"ol-mapbox-style": "^13.0.1",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.8.0",
|
||||
"react-dom": "19.1.0",
|
||||
@ -812,6 +814,11 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@petamoriken/float16": {
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz",
|
||||
"integrity": "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="
|
||||
},
|
||||
"node_modules/@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
@ -2153,6 +2160,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A=="
|
||||
},
|
||||
"node_modules/@types/rbush": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz",
|
||||
"integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||
@ -2825,6 +2837,24 @@
|
||||
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
|
||||
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="
|
||||
},
|
||||
"node_modules/geotiff": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz",
|
||||
"integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==",
|
||||
"dependencies": {
|
||||
"@petamoriken/float16": "^3.4.7",
|
||||
"lerc": "^3.0.0",
|
||||
"pako": "^2.0.4",
|
||||
"parse-headers": "^2.0.2",
|
||||
"quick-lru": "^6.1.1",
|
||||
"web-worker": "^1.2.0",
|
||||
"xml-utils": "^1.0.2",
|
||||
"zstddec": "^0.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.19"
|
||||
}
|
||||
},
|
||||
"node_modules/get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
@ -3007,6 +3037,11 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lerc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz",
|
||||
"integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww=="
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
@ -3276,6 +3311,11 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mapbox-to-css-font": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-3.2.0.tgz",
|
||||
"integrity": "sha512-kvsEfzvLik34BiFj+S19bv5d70l9qSdkUzrq99dvZ9d5POaLyB4vJMQmq3BoJ5D6lFG1GYnMM7o7cm5Jh8YEEg=="
|
||||
},
|
||||
"node_modules/maplibre-gl": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.6.1.tgz",
|
||||
@ -3517,6 +3557,55 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ol": {
|
||||
"version": "10.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ol/-/ol-10.6.1.tgz",
|
||||
"integrity": "sha512-xp174YOwPeLj7c7/8TCIEHQ4d41tgTDDhdv6SqNdySsql5/MaFJEJkjlsYcvOPt7xA6vrum/QG4UdJ0iCGT1cg==",
|
||||
"dependencies": {
|
||||
"@types/rbush": "4.0.0",
|
||||
"earcut": "^3.0.0",
|
||||
"geotiff": "^2.1.3",
|
||||
"pbf": "4.0.1",
|
||||
"rbush": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/openlayers"
|
||||
}
|
||||
},
|
||||
"node_modules/ol-mapbox-style": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-13.0.1.tgz",
|
||||
"integrity": "sha512-NEUT4rpsOCQz5y8qwJikU7UTdX/U8uGvW1we1urZ6NkONFjPRRxqg+PVit7m2AvBeIyrTL3iYd8mHZw8VJEOyw==",
|
||||
"dependencies": {
|
||||
"@maplibre/maplibre-gl-style-spec": "^23.1.0",
|
||||
"mapbox-to-css-font": "^3.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ol/node_modules/pbf": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
||||
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
||||
"dependencies": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"pbf": "bin/pbf"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
|
||||
},
|
||||
"node_modules/parse-headers": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz",
|
||||
"integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A=="
|
||||
},
|
||||
"node_modules/pbf": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
|
||||
@ -3589,6 +3678,17 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/quick-lru": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
|
||||
"integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||
@ -3625,6 +3725,14 @@
|
||||
"webpack": "^4.0.0 || ^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rbush": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz",
|
||||
"integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==",
|
||||
"dependencies": {
|
||||
"quickselect": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
@ -4252,6 +4360,11 @@
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/web-worker": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
|
||||
"integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
@ -4416,6 +4529,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-utils": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
|
||||
"integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA=="
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
@ -4424,6 +4542,11 @@
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/zstddec": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz",
|
||||
"integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg=="
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@ -4844,6 +4967,11 @@
|
||||
"integrity": "sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ==",
|
||||
"optional": true
|
||||
},
|
||||
"@petamoriken/float16": {
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz",
|
||||
"integrity": "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="
|
||||
},
|
||||
"@radix-ui/number": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
|
||||
@ -5602,6 +5730,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
|
||||
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A=="
|
||||
},
|
||||
"@types/rbush": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-4.0.0.tgz",
|
||||
"integrity": "sha512-+N+2H39P8X+Hy1I5mC6awlTX54k3FhiUmvt7HWzGJZvF+syUAAxP/stwppS8JE84YHqFgRMv6fCy31202CMFxQ=="
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "19.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||
@ -6132,6 +6265,21 @@
|
||||
"resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
|
||||
"integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A=="
|
||||
},
|
||||
"geotiff": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.1.3.tgz",
|
||||
"integrity": "sha512-PT6uoF5a1+kbC3tHmZSUsLHBp2QJlHasxxxxPW47QIY1VBKpFB+FcDvX+MxER6UzgLQZ0xDzJ9s48B9JbOCTqA==",
|
||||
"requires": {
|
||||
"@petamoriken/float16": "^3.4.7",
|
||||
"lerc": "^3.0.0",
|
||||
"pako": "^2.0.4",
|
||||
"parse-headers": "^2.0.2",
|
||||
"quick-lru": "^6.1.1",
|
||||
"web-worker": "^1.2.0",
|
||||
"xml-utils": "^1.0.2",
|
||||
"zstddec": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"get-nonce": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||
@ -6256,6 +6404,11 @@
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
||||
},
|
||||
"lerc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lerc/-/lerc-3.0.0.tgz",
|
||||
"integrity": "sha512-Rm4J/WaHhRa93nCN2mwWDZFoRVF18G1f47C+kvQWyHGEZxFpTUi73p7lMVSAndyxGt6lJ2/CFbOcf9ra5p8aww=="
|
||||
},
|
||||
"lightningcss": {
|
||||
"version": "1.30.1",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
|
||||
@ -6378,6 +6531,11 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"mapbox-to-css-font": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-3.2.0.tgz",
|
||||
"integrity": "sha512-kvsEfzvLik34BiFj+S19bv5d70l9qSdkUzrq99dvZ9d5POaLyB4vJMQmq3BoJ5D6lFG1GYnMM7o7cm5Jh8YEEg=="
|
||||
},
|
||||
"maplibre-gl": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.6.1.tgz",
|
||||
@ -6531,6 +6689,47 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"ol": {
|
||||
"version": "10.6.1",
|
||||
"resolved": "https://registry.npmjs.org/ol/-/ol-10.6.1.tgz",
|
||||
"integrity": "sha512-xp174YOwPeLj7c7/8TCIEHQ4d41tgTDDhdv6SqNdySsql5/MaFJEJkjlsYcvOPt7xA6vrum/QG4UdJ0iCGT1cg==",
|
||||
"requires": {
|
||||
"@types/rbush": "4.0.0",
|
||||
"earcut": "^3.0.0",
|
||||
"geotiff": "^2.1.3",
|
||||
"pbf": "4.0.1",
|
||||
"rbush": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"pbf": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz",
|
||||
"integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==",
|
||||
"requires": {
|
||||
"resolve-protobuf-schema": "^2.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ol-mapbox-style": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ol-mapbox-style/-/ol-mapbox-style-13.0.1.tgz",
|
||||
"integrity": "sha512-NEUT4rpsOCQz5y8qwJikU7UTdX/U8uGvW1we1urZ6NkONFjPRRxqg+PVit7m2AvBeIyrTL3iYd8mHZw8VJEOyw==",
|
||||
"requires": {
|
||||
"@maplibre/maplibre-gl-style-spec": "^23.1.0",
|
||||
"mapbox-to-css-font": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"pako": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
|
||||
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="
|
||||
},
|
||||
"parse-headers": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz",
|
||||
"integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A=="
|
||||
},
|
||||
"pbf": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
|
||||
@ -6577,6 +6776,11 @@
|
||||
"resolved": "https://registry.npmjs.org/qss/-/qss-3.0.0.tgz",
|
||||
"integrity": "sha512-ZHoCB3M/3Voev64zhLLUOKDtaEdJ/lymsJJ7R3KBusVZ2ovNiIB7XOq3Xh6V1a8O+Vho+g2B5YElq9zW7D8aQw=="
|
||||
},
|
||||
"quick-lru": {
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-6.1.2.tgz",
|
||||
"integrity": "sha512-AAFUA5O1d83pIHEhJwWCq/RQcRukCkn/NSm2QsTEMle5f2hP0ChI2+3Xb051PZCkLryI/Ir1MVKviT2FIloaTQ=="
|
||||
},
|
||||
"quickselect": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
|
||||
@ -6602,6 +6806,14 @@
|
||||
"schema-utils": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"rbush": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rbush/-/rbush-4.0.1.tgz",
|
||||
"integrity": "sha512-IP0UpfeWQujYC8Jg162rMNc01Rf0gWMMAb2Uxus/Q0qOFw4lCcq6ZnQEZwUoJqWyUGJ9th7JjwI4yIWo+uvoAQ==",
|
||||
"requires": {
|
||||
"quickselect": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
@ -7004,6 +7216,11 @@
|
||||
"graceful-fs": "^4.1.2"
|
||||
}
|
||||
},
|
||||
"web-worker": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz",
|
||||
"integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw=="
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
@ -7118,11 +7335,21 @@
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"requires": {}
|
||||
},
|
||||
"xml-utils": {
|
||||
"version": "1.10.2",
|
||||
"resolved": "https://registry.npmjs.org/xml-utils/-/xml-utils-1.10.2.tgz",
|
||||
"integrity": "sha512-RqM+2o1RYs6T8+3DzDSoTRAUfrvaejbVHcp3+thnAtDKo8LskR+HomLajEy5UjTz24rpka7AxVBRR3g2wTUkJA=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"dev": true
|
||||
},
|
||||
"zstddec": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/zstddec/-/zstddec-0.1.0.tgz",
|
||||
"integrity": "sha512-w2NTI8+3l3eeltKAdK8QpiLo/flRAr2p8AGeakfMZOXBxOg9HIu4LVDxBi81sYgVhFhdJjv1OrB5ssI8uFPoLg=="
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,6 +30,8 @@
|
||||
"lucide-react": "^0.525.0",
|
||||
"maplibre-gl": "^5.6.1",
|
||||
"next": "15.4.1",
|
||||
"ol": "^10.6.1",
|
||||
"ol-mapbox-style": "^13.0.1",
|
||||
"react": "19.1.0",
|
||||
"react-day-picker": "^9.8.0",
|
||||
"react-dom": "19.1.0",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user