Skip to content

Commit

Permalink
return of the swellsim
Browse files Browse the repository at this point in the history
  • Loading branch information
peterbull committed Jan 17, 2025
1 parent 547dc5d commit 12a3c60
Show file tree
Hide file tree
Showing 7 changed files with 818 additions and 20 deletions.
5 changes: 5 additions & 0 deletions frontend/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@
"generate-routes": "@tanstack/router-cli generate-routes"
},
"dependencies": {
"@react-three/drei": "^9.121.2",
"@react-three/fiber": "^8.17.12",
"@tanstack/react-query": "^5.63.0",
"@tanstack/react-router": "^1.95.3",
"@types/three": "^0.172.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-globe.gl": "^2.29.4",
"react-query-devtools": "^2.6.3",
"three": "^0.172.0",
"three-stdlib": "^2.35.7",
"zustand": "^5.0.3"
},
"devDependencies": {
Expand Down
582 changes: 582 additions & 0 deletions frontend/app/pnpm-lock.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/app/src/components/SpotList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useNavigate } from "@tanstack/react-router";
export function SpotList() {
const { globeZoom } = useGlobe();
const navigate = useNavigate();
const { spots, spotsLoading } = useSpots();
const { spots, spotsLoading, setCurrentSpot } = useSpots();
const { searchQuery } = useSearch();

if (spotsLoading) {
Expand Down
189 changes: 189 additions & 0 deletions frontend/app/src/components/SwellSim.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import * as THREE from "three";
import { OrbitControls } from "@react-three/drei";
import { Canvas, useFrame, useLoader, useThree } from "@react-three/fiber";
import circleImg from "../img/circle.png";
import { Suspense, useCallback, useMemo, useRef, useEffect} from "react";
import { useSpotForecasts} from "@/hooks/useSpotForecasts";
import { OrbitControls as OrbitControlsImpl } from 'three-stdlib';
import { useParams } from "@tanstack/react-router";
import { useSpots } from "@/hooks/useSpots";

function CameraControls() {
const {
gl: { domElement },
} = useThree();

const controlsRef = useRef<OrbitControlsImpl>(null);

useEffect(() => {
if (controlsRef.current) {
controlsRef.current.target.set(0, 0, 50);
controlsRef.current.update();
}
}, []);

useEffect(() => {
if (controlsRef.current) {
controlsRef.current.enabled = false;

const enableControls = () => {
if (controlsRef.current) {
controlsRef.current.enabled = true;
}
};

domElement.addEventListener("click", enableControls);

return () => {
domElement.removeEventListener("click", enableControls);
};
}
}, [domElement]);

useFrame(() => {
if (controlsRef.current) {
controlsRef.current.update();
}
});

return (
<OrbitControls
ref={controlsRef}
// args={[camera, domElement]}
// autoRotate
// autoRotateSpeed={-0.2}
/>
);
}


function Points() {
const { spotId } = useParams({
from: '/spots/$spotId',
});
const { spot }= useSpots(Number(spotId));
const { spotForecasts } = useSpotForecasts(spot)
const imgTex = useLoader(THREE.TextureLoader, circleImg);
const bufferRef = useRef<THREE.BufferAttribute>(null);
const tRef = useRef(0);

const forecastRef = useRef({
waveSpeed: 0,
period: 0,
amplitude: 0
});

useEffect(() => {
const currentForecast = spotForecasts?.[0];
if (currentForecast) {
const feetFactor = 3.28084;
forecastRef.current = {
waveSpeed: 1 / ((currentForecast?.swper ?? currentForecast?.perpw) ?? 1),
period: currentForecast?.swper ?? currentForecast?.perpw ?? 1,
amplitude: (currentForecast?.swh ?? 1) * feetFactor
};
}
}, [spotForecasts]);

const graph = useCallback((z: number) => {
const { waveSpeed, period, amplitude } = forecastRef.current;
const waveFrequency = 1 / period;
const y = amplitude * Math.sin(waveFrequency * z + (waveSpeed + tRef.current) / 2);
return y > 0 ? y : 0;
}, []);

const count = 200;
const sep = 1;

// initial positions
const positions = useMemo(() => {
const pos = new Float32Array(count * count * 3);
let i = 0;
for (let xi = 0; xi < count; xi++) {
for (let zi = 0; zi < count; zi++) {
pos[i] = sep * (xi - count / 2);
pos[i + 1] = 0;
pos[i + 2] = sep * zi;
i += 3;
}
}
return pos;
}, [count, sep]);

// create buffer
useEffect(() => {
if (bufferRef.current) {
bufferRef.current.array = positions;
bufferRef.current.needsUpdate = true;
}
}, [positions]);

useFrame((state) => {
tRef.current = state.clock.getElapsedTime();

if (!bufferRef.current) return;

const positions = bufferRef.current.array;
let i = 0;
for (let xi = 0; xi < count; xi++) {
for (let zi = 0; zi < count; zi++) {
positions[i + 1] = graph(sep * zi);
i += 3;
}
}
bufferRef.current.needsUpdate = true;
});

if (!spotForecasts?.[0]) return null;

return (
<points>
<bufferGeometry attach="geometry">
<bufferAttribute
ref={bufferRef}
attach="attributes-position"
array={positions}
count={positions.length / 3}
itemSize={3}
/>
</bufferGeometry>

<pointsMaterial
attach="material"
map={imgTex}
color={0x03e9f4}
size={0.8}
sizeAttenuation
transparent={false}
alphaTest={0.5}
opacity={1.0}
/>
</points>
);
}



function AnimationCanvas() {
return (
<Canvas camera={{ position: [10, 20, 0], fov: 50 }}>
<Suspense fallback={null}>
<axesHelper args={[5]} />
<Points />
</Suspense>
<CameraControls />
</Canvas>
);
}

const SwellSim = () => {
return (
<div className="anim pb-10 h-[300px]">
<Suspense fallback={<div>Loading...</div>}>
<AnimationCanvas />
</Suspense>
</div>
);
};

export default SwellSim;
2 changes: 1 addition & 1 deletion frontend/app/src/hooks/useGlobe.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Spot } from './useSpots';
import { Spot } from '@/hooks/useSpots';
import { GlobeMethods } from 'react-globe.gl';
import { useQuery, useQueryClient } from '@tanstack/react-query';

Expand Down
47 changes: 33 additions & 14 deletions frontend/app/src/hooks/useSpots.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { create } from "zustand";

export type Spot = {
id: number;
Expand All @@ -8,11 +9,21 @@ export type Spot = {
street_address: string;
};

interface SpotStore {
currentSpot: Spot | null;
setCurrentSpot: (spot: Spot | null) => void;
}

const useSpotStore = create<SpotStore>((set) => ({
currentSpot: null,
setCurrentSpot: (spot) => set({ currentSpot: spot }),
}));

async function fetchSpot(spotId: number): Promise<Spot> {
const res = await fetch(
`${import.meta.env.REACT_APP_BACKEND_URL ?? "http://localhost:8000"}/spots/${spotId}`
);
if (!res.ok) {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const data = await res.json();
Expand All @@ -30,27 +41,35 @@ async function fetchSpots(): Promise<Spot[]> {
return data;
}

export function useSpot(spotId: number) {
export function useSpots(spotId?: number) {
const spotsQuery = useQuery({
queryKey: ["spots"],
queryFn: fetchSpots
});

const spotQuery = useQuery({
queryKey: ["spot", spotId],
queryFn: () => fetchSpot(spotId),
enabled: !!spotId,
queryFn: () => fetchSpot(spotId!),
enabled: !!spotId,
});

return {
spot: spotQuery.data,
isLoading: spotQuery.isLoading,
isError: spotQuery.isError,
error: spotQuery.error,
};
}
const currentSpot = useSpotStore((state) => state.currentSpot);
const setCurrentSpot = useSpotStore((state) => state.setCurrentSpot);

export function useSpots() {
const spotsQuery = useQuery({ queryKey: ["spots"], queryFn: fetchSpots });

return {
// List of spots
spots: spotsQuery.data ?? [],
spotsLoading: spotsQuery.isLoading,
spotsError: spotsQuery.isError,

// Single spot
spot: spotQuery.data,
spotLoading: spotQuery.isLoading,
spotError: spotQuery.isError,
spotErrorMessage: spotQuery.error,

// Current selected spot
currentSpot,
setCurrentSpot,
};
}
11 changes: 7 additions & 4 deletions frontend/app/src/routes/spots.$spotId.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import SwellSim from '@/components/SwellSim';
import { useSpotForecasts } from '@/hooks/useSpotForecasts';
import { useSpot } from '@/hooks/useSpots';
import { useSpots } from '@/hooks/useSpots';
import { createFileRoute, useParams } from '@tanstack/react-router'

export const Route = createFileRoute('/spots/$spotId')({
component: RouteComponent,

})

function RouteComponent() {
const { spotId } = useParams({
from: '/spots/$spotId',
});
const id = Number(spotId)
const { spot } = useSpot(id)
const { spot } = useSpots(id)
const { spotForecasts } = useSpotForecasts(spot);

return <div>{`hello ${spot?.spot_name}, here are some ${JSON.stringify(spotForecasts[0])}`}</div>
return (
<SwellSim />
)
}

0 comments on commit 12a3c60

Please sign in to comment.