Skip to content

Commit 10134e6

Browse files
committed
real data fetch
1 parent 16a44c4 commit 10134e6

File tree

13 files changed

+5860
-413
lines changed

13 files changed

+5860
-413
lines changed

.env.local.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Mapbox Configuration
2+
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=your_mapbox_token_here
3+
4+
# Auth0 Configuration
5+
AUTH0_CLIENT_ID=your_auth0_client_id
6+
AUTH0_CLIENT_SECRET=your_auth0_client_secret
7+
AUTH0_ISSUER_BASE_URL=your_auth0_domain
8+
AUTH0_SECRET=your_random_secret_key
9+
10+
# CloudCasting API Configuration
11+
NEXT_PUBLIC_CLOUDCASTING_API_URL=http://0.0.0.0:8000/api/cloudcasting/layers

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,17 @@ AUTH0_BASE_URL=http://localhost:3000
5757
AUTH0_SCOPE=openid profile email
5858
5959
# Mapbox Configuration
60+
```
61+
6062
NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN=your_mapbox_token
63+
AUTH0_CLIENT_ID=your_auth0_client_id
64+
AUTH0_CLIENT_SECRET=your_auth0_client_secret
65+
AUTH0_ISSUER_BASE_URL=your_auth0_domain
66+
AUTH0_SECRET=your_random_secret
67+
NEXT_PUBLIC_CLOUDCASTING_API_URL=http://localhost:8000/api/cloudcasting/layers
68+
69+
```
70+
6171
```
6272

6373
### Installation
@@ -89,6 +99,9 @@ Authentication is handled via Auth0 integration with NextAuth.js. Protected rout
8999
## Features
90100

91101
- **Interactive Map**: Visualizes cloud movements using Mapbox GL
102+
- **Real-time Cloud Layers**: Fetch and display satellite imagery in multiple spectral bands (IR, VIS, WV)
103+
- **Time-series Forecasting**: View predicted cloud positions at 15-minute intervals up to 3 hours ahead
104+
- **Variable Selection**: Choose from 10 different satellite bands for cloud analysis
92105
- **Authentication**: Secure login via Auth0
93106
- **Responsive Design**: Works across different device sizes
94107
- **Dark Mode**: Optimized for viewing satellite imagery
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
const API_BASE_URL =
4+
process.env.CLOUDCASTING_API_URL || 'http://localhost:8000/api/cloudcasting/layers';
5+
6+
export async function GET(
7+
request: NextRequest,
8+
{ params }: { params: { variable: string; step: string } }
9+
) {
10+
try {
11+
const { variable, step } = params;
12+
13+
// Validate parameters
14+
if (!variable || !step) {
15+
return NextResponse.json({ error: 'Missing variable or step parameter' }, { status: 400 });
16+
}
17+
18+
// Validate step is a number
19+
const stepNum = parseInt(step.replace('.tif', ''));
20+
if (isNaN(stepNum) || stepNum < 0 || stepNum > 11) {
21+
return NextResponse.json(
22+
{ error: 'Invalid step parameter. Must be between 0 and 11' },
23+
{ status: 400 }
24+
);
25+
}
26+
27+
// Fetch from the CloudCasting API
28+
const apiUrl = `${API_BASE_URL}/${variable}/${stepNum}.tif`;
29+
const response = await fetch(apiUrl, {
30+
method: 'GET',
31+
headers: {
32+
Accept: 'application/octet-stream',
33+
},
34+
});
35+
36+
if (!response.ok) {
37+
console.error(`API request failed: ${response.status} ${response.statusText}`);
38+
return NextResponse.json(
39+
{ error: `Failed to fetch from CloudCasting API: ${response.statusText}` },
40+
{ status: response.status }
41+
);
42+
}
43+
44+
// Get the blob data
45+
const blob = await response.blob();
46+
const arrayBuffer = await blob.arrayBuffer();
47+
48+
// Return the TIF file with proper headers
49+
return new NextResponse(arrayBuffer, {
50+
status: 200,
51+
headers: {
52+
'Content-Type': 'image/tiff',
53+
'Content-Length': arrayBuffer.byteLength.toString(),
54+
'Cache-Control': 'public, max-age=300', // Cache for 5 minutes
55+
'Access-Control-Allow-Origin': '*',
56+
'Access-Control-Allow-Methods': 'GET',
57+
'Access-Control-Allow-Headers': 'Content-Type',
58+
},
59+
});
60+
} catch (error) {
61+
console.error('Error in cloud layer proxy:', error);
62+
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
63+
}
64+
}
65+
66+
// Handle preflight requests
67+
export async function OPTIONS() {
68+
return new NextResponse(null, {
69+
status: 200,
70+
headers: {
71+
'Access-Control-Allow-Origin': '*',
72+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
73+
'Access-Control-Allow-Headers': 'Content-Type',
74+
},
75+
});
76+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
const API_BASE_URL =
4+
process.env.CLOUDCASTING_API_URL || 'http://localhost:8000/api/cloudcasting/layers';
5+
6+
export async function GET(
7+
request: NextRequest,
8+
{ params }: { params: { variable: string; step: string } }
9+
) {
10+
try {
11+
const { variable, step } = params;
12+
13+
// Validate parameters
14+
if (!variable || !step) {
15+
return NextResponse.json({ error: 'Missing variable or step parameter' }, { status: 400 });
16+
}
17+
18+
// Validate step is a number
19+
const stepNum = parseInt(step.replace('.tif', ''));
20+
if (isNaN(stepNum) || stepNum < 0 || stepNum > 11) {
21+
return NextResponse.json(
22+
{ error: 'Invalid step parameter. Must be between 0 and 11' },
23+
{ status: 400 }
24+
);
25+
}
26+
27+
// Fetch from the CloudCasting API
28+
const apiUrl = `${API_BASE_URL}/${variable}/${stepNum}.tif`;
29+
console.log(`Proxying request to: ${apiUrl}`);
30+
31+
const response = await fetch(apiUrl, {
32+
method: 'GET',
33+
headers: {
34+
Accept: 'application/octet-stream',
35+
'User-Agent': 'CloudCasting-UI-Proxy/1.0',
36+
},
37+
});
38+
39+
if (!response.ok) {
40+
console.error(`API request failed: ${response.status} ${response.statusText}`);
41+
return NextResponse.json(
42+
{ error: `Failed to fetch from CloudCasting API: ${response.statusText}` },
43+
{ status: response.status }
44+
);
45+
}
46+
47+
// Get the blob data
48+
const blob = await response.blob();
49+
const arrayBuffer = await blob.arrayBuffer();
50+
51+
// Return the TIF file with proper headers
52+
return new NextResponse(arrayBuffer, {
53+
status: 200,
54+
headers: {
55+
'Content-Type': 'image/tiff',
56+
'Content-Length': arrayBuffer.byteLength.toString(),
57+
'Cache-Control': 'public, max-age=300', // Cache for 5 minutes
58+
'Access-Control-Allow-Origin': '*',
59+
'Access-Control-Allow-Methods': 'GET',
60+
'Access-Control-Allow-Headers': 'Content-Type',
61+
},
62+
});
63+
} catch (error) {
64+
console.error('Error in cloud layer proxy:', error);
65+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
66+
return NextResponse.json(
67+
{ error: 'Internal server error', details: errorMessage },
68+
{ status: 500 }
69+
);
70+
}
71+
}
72+
73+
// Handle preflight requests
74+
export async function OPTIONS() {
75+
return new NextResponse(null, {
76+
status: 200,
77+
headers: {
78+
'Access-Control-Allow-Origin': '*',
79+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
80+
'Access-Control-Allow-Headers': 'Content-Type',
81+
},
82+
});
83+
}

app/globals.css

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,29 @@ body {
2424
color: var(--foreground);
2525
font-family: Inter, sans-serif;
2626
}
27+
28+
/* Custom slider styles */
29+
.slider {
30+
background: linear-gradient(to right, #3b82f6 0%, #1e40af 100%);
31+
}
32+
33+
.slider::-webkit-slider-thumb {
34+
appearance: none;
35+
height: 20px;
36+
width: 20px;
37+
border-radius: 50%;
38+
background: #ffffff;
39+
cursor: pointer;
40+
border: 2px solid #3b82f6;
41+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
42+
}
43+
44+
.slider::-moz-range-thumb {
45+
height: 20px;
46+
width: 20px;
47+
border-radius: 50%;
48+
background: #ffffff;
49+
cursor: pointer;
50+
border: 2px solid #3b82f6;
51+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
52+
}

app/page.tsx

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
'use client';
22

3-
import { useRef, useEffect } from 'react';
3+
import { useRef, useEffect, useState } from 'react';
44
import mapboxgl from 'mapbox-gl';
55
import 'mapbox-gl/dist/mapbox-gl.css';
66
import Header from '@/components/navbar';
7+
import CloudLayerControls from '@/components/cloud-layer-controls';
78
import { withAuth } from '@/utils/withAuth';
89

910
const HomePage = () => {
1011
const mapRef = useRef<mapboxgl.Map | null>(null);
1112
const mapContainerRef = useRef(null);
13+
const [mapLoaded, setMapLoaded] = useState(false);
1214

1315
useEffect(() => {
1416
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN || '';
@@ -24,10 +26,7 @@ const HomePage = () => {
2426
style: 'mapbox://styles/mapbox/dark-v11',
2527
center: [-2.5, 54],
2628
zoom: 5,
27-
projection: 'globe',
28-
minZoom: 5,
29-
maxZoom: 6,
30-
maxBounds: bounds,
29+
projection: 'equirectangular',
3130
});
3231

3332
if (mapRef.current) {
@@ -39,9 +38,7 @@ const HomePage = () => {
3938
'space-color': 'rgb(11, 11, 25)',
4039
'star-intensity': 0.6,
4140
});
42-
43-
mapRef.current?.setPitch(30);
44-
mapRef.current?.setBearing(-15);
41+
setMapLoaded(true);
4542
});
4643
}
4744
}
@@ -56,7 +53,14 @@ const HomePage = () => {
5653
return (
5754
<div>
5855
<Header />
59-
<div id="map" ref={mapContainerRef} style={{ width: '100%', height: 'calc(100vh - 64px)' }} />
56+
<div className="relative">
57+
<div
58+
id="map"
59+
ref={mapContainerRef}
60+
style={{ width: '100%', height: 'calc(100vh - 64px)' }}
61+
/>
62+
{mapLoaded && <CloudLayerControls map={mapRef.current} />}
63+
</div>
6064
</div>
6165
);
6266
};

0 commit comments

Comments
 (0)