Skip to content

Commit ea0bcab

Browse files
Merge pull request #2 from Santiago13dev/santidev
Santidev
2 parents 42b3cdc + 7c2c943 commit ea0bcab

File tree

15 files changed

+344
-224
lines changed

15 files changed

+344
-224
lines changed

.gitignore

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
1-
# Frontend
2-
frontend/node_modules/
3-
frontend/.next/
4-
frontend/.turbo/
5-
frontend/dist/
6-
frontend/out/
7-
frontend/.vercel/
8-
9-
# Backend
10-
backend/.gradle/
11-
backend/build/
12-
backend/out/
13-
backend/logs/
14-
15-
# General
16-
*.log
17-
.env
18-
19-
# Node/Next (por tu frontend)
1+
# --- Node / Next.js ---
202
node_modules/
213
.next/
4+
out/
225
dist/
23-
out/
6+
npm-debug.log*
7+
yarn-debug.log*
8+
yarn-error.log*
9+
pnpm-debug.log*
10+
.vercel/
11+
.env
12+
.env.*
13+
!.env.example
14+
coverage/
15+
16+
# --- Java / Gradle ---
17+
.gradle/
18+
build/
19+
!gradle/wrapper/gradle-wrapper.jar
20+
!gradle/wrapper/gradle-wrapper.properties
21+
# IDE
22+
.idea/
23+
*.iml
24+
# Compilados
25+
*.class
26+
27+
# --- VS Code (opcional) ---
28+
.vscode/
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

backend/.gradle/file-system.probe

0 Bytes
Binary file not shown.

frontend/app/page.tsx

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import { useState, useMemo } from "react";
3+
import { useState, useMemo, useEffect } from "react";
44
import dynamic from "next/dynamic";
55

66
const Globe = dynamic(() => import("../components/Globe"), { ssr: false });
@@ -9,7 +9,10 @@ const Stats = dynamic(() => import("../components/Stats"), { ssr: false });
99
const API_BASE = process.env.NEXT_PUBLIC_API_BASE ?? "http://localhost:8080";
1010

1111
type SimResponse = {
12-
rings?: { P: { minutes: number; radiusKm: number }[]; S: { minutes: number; radiusKm: number }[] };
12+
rings?: {
13+
P: { minutes: number; radiusKm: number }[];
14+
S: { minutes: number; radiusKm: number }[];
15+
};
1316
arrivals?: { place: string; type: "P" | "S"; minutes: number }[];
1417
intensity?: { gridId: string; legend: { label: string; colorHex: string }[] };
1518
};
@@ -22,13 +25,48 @@ export default function Home() {
2225
const [resp, setResp] = useState<SimResponse | null>(null);
2326
const [error, setError] = useState<string | null>(null);
2427

28+
// ✅ Chequeo de existencia de texturas para evitar crash del loader
29+
const [texOk, setTexOk] = useState<boolean | null>(null);
30+
useEffect(() => {
31+
let alive = true;
32+
(async () => {
33+
try {
34+
const urls = [
35+
"/textures/earth_political_4k.jpg",
36+
"/textures/earth_normal_2k.jpg",
37+
"/textures/earth_specular_2k.jpg",
38+
];
39+
const resps = await Promise.all(
40+
urls.map((u) => fetch(u, { method: "HEAD", cache: "no-store" }))
41+
);
42+
if (!alive) return;
43+
setTexOk(resps.every((r) => r.ok));
44+
} catch {
45+
if (!alive) return;
46+
setTexOk(false);
47+
}
48+
})();
49+
return () => {
50+
alive = false;
51+
};
52+
}, []);
53+
54+
// Centro del sismo
2555
const center = useMemo(() => {
2656
const la = Number(lat);
2757
const lo = Number(lon);
2858
if (Number.isFinite(la) && Number.isFinite(lo)) return { lat: la, lon: lo };
2959
return null;
3060
}, [lat, lon]);
3161

62+
// Adaptar formato para el globo
63+
const ringsForGlobe = useMemo(() => {
64+
const p = resp?.rings?.P?.map((r) => r.radiusKm) ?? [];
65+
const s = resp?.rings?.S?.map((r) => r.radiusKm) ?? [];
66+
if (p.length === 0 && s.length === 0) return null;
67+
return { p, s };
68+
}, [resp]);
69+
3270
async function simular() {
3371
setLoading(true);
3472
setError(null);
@@ -63,6 +101,7 @@ export default function Home() {
63101
const txt = await r.text().catch(() => "");
64102
throw new Error(`HTTP ${r.status} ${txt}`);
65103
}
104+
66105
const data: SimResponse = await r.json();
67106
setResp(data);
68107
console.log("Simulación OK:", data);
@@ -125,20 +164,39 @@ export default function Home() {
125164
<div className="max-w-6xl mx-auto p-4 grid grid-cols-1 lg:grid-cols-3 gap-4">
126165
<section className="lg:col-span-2">
127166
<div className="relative h-[60vh] rounded-xl border border-slate-800 overflow-hidden bg-black">
128-
{/* Importante: evita que el Canvas tape los controles */}
129-
{/* El Canvas está solo dentro de este contenedor, bajo el header */}
130-
{center && (
167+
{/* Si faltan texturas, avisamos cómo resolver */}
168+
{texOk === false && (
169+
<div className="absolute inset-0 grid place-items-center text-center p-6">
170+
<div>
171+
<p className="text-red-300 font-semibold">Faltan texturas del globo</p>
172+
<p className="text-slate-300 text-sm mt-2">
173+
Copia estos archivos en <code>/public/textures</code>:
174+
</p>
175+
<pre className="text-xs mt-2 bg-slate-800/70 p-3 rounded">
176+
{`/public/textures/earth_political_4k.jpg
177+
/public/textures/earth_normal_2k.jpg
178+
/public/textures/earth_specular_2k.jpg`}
179+
</pre>
180+
<p className="text-slate-400 text-xs mt-2">
181+
Luego abre en el navegador:
182+
<br />
183+
<code>/textures/earth_political_4k.jpg</code>
184+
</p>
185+
</div>
186+
</div>
187+
)}
188+
189+
{/* Montamos el globo solo si las texturas existen */}
190+
{texOk && (
131191
<Globe
192+
key={center ? `${center.lat},${center.lon}` : "no-center"}
132193
center={center}
133-
rings={resp?.rings ?? null}
134-
intensity={resp?.intensity ?? null}
194+
rings={ringsForGlobe}
135195
/>
136196
)}
137197
</div>
138198

139-
{error && (
140-
<p className="mt-3 text-red-400 break-words">Error: {error}</p>
141-
)}
199+
{error && <p className="mt-3 text-red-400 break-words">Error: {error}</p>}
142200
</section>
143201

144202
<aside className="lg:col-span-1">

frontend/components/Globe.tsx

Lines changed: 58 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
11
"use client";
2+
23
import * as THREE from "three";
3-
import { Canvas, useFrame, useThree } from "@react-three/fiber";
4+
import { Canvas, useFrame } from "@react-three/fiber";
45
import { OrbitControls, Stars, useTexture } from "@react-three/drei";
56
import { useMemo, useRef } from "react";
67

78
/** Props mínimas que ya usabas */
89
export type Center = { lat: number; lon: number };
910
export type RingSet = { p?: number[]; s?: number[] }; // kilómetros (puede venir vacío)
1011

11-
type GlobeProps = {
12-
center: Center;
13-
rings?: RingSet | null;
14-
};
12+
type GlobeProps = { center?: Center | null; rings?: RingSet | null };
1513

1614
/* ------------------------- utilidades geo ------------------------- */
1715
const d2r = (d: number) => (d * Math.PI) / 180;
@@ -50,8 +48,7 @@ function destinationPoint(lat: number, lon: number, bearingDeg: number, angDistR
5048
/* --------------------------- materiales -------------------------- */
5149

5250
function Atmosphere() {
53-
// shader muy simple de “rim light” para la atmósfera
54-
const materialRef = useRef<THREE.ShaderMaterial>(null!);
51+
// shader muy simple de “rim light”
5552
const vertexShader = /* glsl */ `
5653
varying vec3 vNormal;
5754
void main() {
@@ -70,7 +67,6 @@ function Atmosphere() {
7067
<mesh scale={1.018}>
7168
<sphereGeometry args={[1, 64, 64]} />
7269
<shaderMaterial
73-
ref={materialRef}
7470
vertexShader={vertexShader}
7571
fragmentShader={fragmentShader}
7672
blending={THREE.AdditiveBlending}
@@ -87,21 +83,18 @@ function Graticule({ color = "#2dd4bf", alpha = 0.25 }) {
8783
const lines = useMemo(() => {
8884
const group: JSX.Element[] = [];
8985
const mat = new THREE.LineBasicMaterial({ color, transparent: true, opacity: alpha });
86+
9087
// paralelos cada 30°
9188
for (let lat = -60; lat <= 60; lat += 30) {
9289
const pts: THREE.Vector3[] = [];
93-
for (let lon = -180; lon <= 180; lon += 3) {
94-
pts.push(latLonToVector3(1.001, lat, lon));
95-
}
90+
for (let lon = -180; lon <= 180; lon += 3) pts.push(latLonToVector3(1.001, lat, lon));
9691
const geo = new THREE.BufferGeometry().setFromPoints(pts);
9792
group.push(<line key={`par-${lat}`} geometry={geo} material={mat} />);
9893
}
9994
// meridianos cada 30°
10095
for (let lon = -150; lon <= 180; lon += 30) {
10196
const pts: THREE.Vector3[] = [];
102-
for (let lat = -90; lat <= 90; lat += 3) {
103-
pts.push(latLonToVector3(1.001, lat, lon));
104-
}
97+
for (let lat = -90; lat <= 90; lat += 3) pts.push(latLonToVector3(1.001, lat, lon));
10598
const geo = new THREE.BufferGeometry().setFromPoints(pts);
10699
group.push(<line key={`mer-${lon}`} geometry={geo} material={mat} />);
107100
}
@@ -143,7 +136,7 @@ function Stand() {
143136
/* ----------------------------- Tierra ----------------------------- */
144137

145138
function EarthBall() {
146-
// Carga de texturas (las opcionales no son obligatorias)
139+
// Carga de texturas desde /public/textures
147140
const [colorMap, normalMap, specMap] = useTexture(
148141
[
149142
"/textures/earth_political_4k.jpg",
@@ -153,17 +146,18 @@ function EarthBall() {
153146
(txs) => txs.forEach((t) => (t.anisotropy = 8))
154147
);
155148

149+
// El mapa "specular" es para Phong, no para Standard.
150+
const hasNormal = (normalMap as any)?.image;
151+
const hasSpec = (specMap as any)?.image;
152+
156153
return (
157154
<mesh>
158155
<sphereGeometry args={[1, 128, 128]} />
159-
<meshStandardMaterial
160-
map={colorMap}
161-
normalMap={(normalMap as any)?.image ? (normalMap as THREE.Texture) : undefined}
162-
metalnessMap={(specMap as any)?.image ? (specMap as THREE.Texture) : undefined}
163-
roughnessMap={(specMap as any)?.image ? (specMap as THREE.Texture) : undefined}
164-
metalness={0.1}
165-
roughness={0.9}
166-
envMapIntensity={0.7}
156+
<meshPhongMaterial
157+
map={colorMap as THREE.Texture}
158+
normalMap={hasNormal ? (normalMap as THREE.Texture) : undefined}
159+
specularMap={hasSpec ? (specMap as THREE.Texture) : undefined}
160+
shininess={12}
167161
/>
168162
</mesh>
169163
);
@@ -210,41 +204,46 @@ function RingCircle({
210204
const geo = useMemo(() => new THREE.BufferGeometry().setFromPoints(points), [points]);
211205
return (
212206
<line geometry={geo}>
213-
<lineBasicMaterial color={color} linewidth={2} />
207+
<lineBasicMaterial color={color} />
214208
</line>
215209
);
216210
}
217211

218-
/* ---------------------------- Controls ---------------------------- */
212+
/* ------------------- World: vive DENTRO del Canvas ------------------- */
219213

220-
function Controls() {
221-
const { camera } = useThree();
222-
// ligera inclinación estilo “eje terrestre”
223-
const group = useRef<THREE.Group>(null!);
224-
useFrame(() => {
225-
if (!group.current) return;
226-
group.current.rotation.z = d2r(23.4);
214+
function World({ center, rings }: { center: Center; rings?: RingSet | null }) {
215+
const worldRef = useRef<THREE.Group>(null!);
216+
217+
// Animación: tilt + rotación suave
218+
useFrame((_, delta) => {
219+
if (!worldRef.current) return;
220+
worldRef.current.rotation.z = d2r(23.4);
221+
worldRef.current.rotation.y += delta * 0.05;
227222
});
223+
228224
return (
229-
<>
230-
<group ref={group} />
231-
<OrbitControls
232-
enableDamping
233-
dampingFactor={0.08}
234-
minDistance={1.6}
235-
maxDistance={6}
236-
rotateSpeed={0.8}
237-
zoomSpeed={0.9}
238-
enablePan={false}
239-
target={[0, 0, 0]}
240-
/>
241-
</>
225+
<group ref={worldRef}>
226+
<EarthBall />
227+
<Atmosphere />
228+
<Graticule />
229+
<EquatorNeon />
230+
<Pin lat={center.lat} lon={center.lon} />
231+
{rings?.p?.map((r, i) => (
232+
<RingCircle key={`p-${i}`} center={center} radiusKm={r} color="#7cf9f1" />
233+
))}
234+
{rings?.s?.map((r, i) => (
235+
<RingCircle key={`s-${i}`} center={center} radiusKm={r} color="#ff8ec9" />
236+
))}
237+
</group>
242238
);
243239
}
244240

245241
/* ============================ GLOBE ============================ */
246242

247243
export default function Globe({ center, rings }: GlobeProps) {
244+
// 🔒 Si aún no hay center (hidratación/primer render), no dibujes nada
245+
if (!center) return null;
246+
248247
return (
249248
<Canvas
250249
camera={{ position: [0, 0, 3.1], fov: 42 }}
@@ -258,28 +257,24 @@ export default function Globe({ center, rings }: GlobeProps) {
258257
<pointLight position={[-4, -3, -4]} intensity={0.5} />
259258

260259
{/* Estrellas sutiles */}
261-
<Stars radius={80} depth={35} count={2000} factor={2} fade speed={0.2} />
262-
263-
{/* Globo */}
264-
<group>
265-
<EarthBall />
266-
<Atmosphere />
267-
<Graticule />
268-
<EquatorNeon />
269-
<Pin lat={center.lat} lon={center.lon} />
270-
{/* Anillos P/S si existen */}
271-
{rings?.p?.map((r, i) => (
272-
<RingCircle key={`p-${i}`} center={center} radiusKm={r} color="#7cf9f1" />
273-
))}
274-
{rings?.s?.map((r, i) => (
275-
<RingCircle key={`s-${i}`} center={center} radiusKm={r} color="#ff8ec9" />
276-
))}
277-
</group>
260+
<Stars radius={80} depth={35} count={1600} factor={2} fade speed={0.2} />
261+
262+
{/* Globo + elementos */}
263+
<World center={center} rings={rings} />
278264

279265
{/* Soporte / base (decorativo) */}
280266
<Stand />
281267

282-
<Controls />
268+
<OrbitControls
269+
enableDamping
270+
dampingFactor={0.08}
271+
minDistance={1.6}
272+
maxDistance={6}
273+
rotateSpeed={0.8}
274+
zoomSpeed={0.9}
275+
enablePan={false}
276+
target={[0, 0, 0]}
277+
/>
283278
</Canvas>
284279
);
285280
}

frontend/next.config.cjs

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)