diff --git a/.env b/.env index b454ca74..0ad5ec11 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ DATABASE_URL="postgresql://user:password@host:port/db" +WINDY_API_KEY="YOUR_WINDY_API_KEY" diff --git a/.env.local.example b/.env.local.example index 485fd803..209643e3 100644 --- a/.env.local.example +++ b/.env.local.example @@ -3,6 +3,9 @@ SMITHERY_PROFILE_ID="your_smithery_profile_id_here" SMITHERY_API_KEY="your_smithery_api_key_here" +# Windy.com Webcams API Key +WINDY_API_KEY="your_windy_api_key_here" + # NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN is already used by mapbox-map.tsx # Ensure it's also in your .env.local file if you haven't set it up yet. # NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN="your_mapbox_public_token_here" diff --git a/components/header.tsx b/components/header.tsx index 0a370612..354637d8 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -7,12 +7,12 @@ import { Button } from '@/components/ui/button' import { Search, CircleUserRound, - Map, CalendarDays, - TentTree + TentTree, } from 'lucide-react' import { MapToggle } from './map-toggle' import { ProfileToggle } from './profile-toggle' +import { SensorToggle } from './sensor-toggle' export const Header = () => { return ( @@ -43,9 +43,7 @@ export const Header = () => { - + diff --git a/components/map-toggle-context.tsx b/components/map-toggle-context.tsx index e3d3b552..c225d35e 100644 --- a/components/map-toggle-context.tsx +++ b/components/map-toggle-context.tsx @@ -6,6 +6,7 @@ export enum MapToggleEnum { FreeMode, RealTimeMode, DrawingMode, // Added new mode for drawing and measurement + SensorMode, } interface MapToggleContextType { diff --git a/components/map-toggle.tsx b/components/map-toggle.tsx index 1ad35f59..fe441462 100644 --- a/components/map-toggle.tsx +++ b/components/map-toggle.tsx @@ -17,9 +17,8 @@ export function MapToggle() { return ( - @@ -30,7 +29,7 @@ export function MapToggle() { My Maps {setMapType(MapToggleEnum.DrawingMode)}}> - + Draw & Measure diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx index e9ace780..83592f59 100644 --- a/components/map/mapbox-map.tsx +++ b/components/map/mapbox-map.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useRef, useCallback } from 'react' // Removed useState +import { useEffect, useRef, useCallback, useState } from 'react' import mapboxgl from 'mapbox-gl' import MapboxDraw from '@mapbox/mapbox-gl-draw' import * as turf from '@turf/turf' @@ -11,6 +11,7 @@ import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css' import { useMapToggle, MapToggleEnum } from '../map-toggle-context' import { useMapData } from './map-data-context'; // Add this import import { useMapLoading } from '../map-loading-context'; // Import useMapLoading +import { getSensors, Sensor } from '@/lib/sensors'; mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string; @@ -32,6 +33,9 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number const { mapData, setMapData } = useMapData(); // Consume the new context, get setMapData const { setIsMapLoaded } = useMapLoading(); // Get setIsMapLoaded from context const previousMapTypeRef = useRef(null) + const [sensors, setSensors] = useState([]); + const sensorMarkersRef = useRef<{ [id: string]: mapboxgl.Marker }>({}); + // Refs for long-press functionality const longPressTimerRef = useRef(null); @@ -505,6 +509,58 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number } }, [position, updateMapPosition, mapType]) + useEffect(() => { + if (mapType === MapToggleEnum.SensorMode) { + const fetchSensors = async () => { + try { + const windyResponse = await getSensors(); + setSensors(windyResponse.webcams); + } catch (error) { + console.error('Error fetching sensors:', error); + toast.error('Failed to fetch sensors.'); + } + }; + fetchSensors(); + } else { + // Clear sensors and markers when not in SensorMode + setSensors([]); + Object.values(sensorMarkersRef.current).forEach(marker => marker.remove()); + sensorMarkersRef.current = {}; + } + }, [mapType]); + + useEffect(() => { + if (map.current && mapType === MapToggleEnum.SensorMode) { + // Clear existing markers + Object.values(sensorMarkersRef.current).forEach(marker => marker.remove()); + sensorMarkersRef.current = {}; + + sensors.forEach(sensor => { + const el = document.createElement('div'); + el.className = 'sensor-marker'; + el.style.width = '30px'; + el.style.height = '30px'; + el.style.borderRadius = '50%'; + el.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; + el.style.color = 'white'; + el.style.display = 'flex'; + el.style.justifyContent = 'center'; + el.style.alignItems = 'center'; + el.style.cursor = 'pointer'; + el.innerHTML = '↗'; + + const marker = new mapboxgl.Marker(el) + .setLngLat([sensor.location.longitude, sensor.location.latitude]) + .setPopup(new mapboxgl.Popup({ offset: 25 }).setHTML( + `

${sensor.title}

${sensor.title}
` + )) + .addTo(map.current!); + + sensorMarkersRef.current[sensor.id] = marker; + }); + } + }, [sensors, mapType]); + // Effect to handle map updates from MapDataContext useEffect(() => { if (mapData.targetPosition && map.current) { diff --git a/components/mobile-icons-bar.tsx b/components/mobile-icons-bar.tsx index a5a2af9a..97a25f4c 100644 --- a/components/mobile-icons-bar.tsx +++ b/components/mobile-icons-bar.tsx @@ -9,14 +9,14 @@ import { CircleUserRound, Map, CalendarDays, - TentTree, Paperclip, ArrowRight, Plus } from 'lucide-react' import { History } from '@/components/history' -import { MapToggle } from './map-toggle' import { ModeToggle } from './mode-toggle' +import { MapToggle } from './map-toggle' +import { SensorToggle } from './sensor-toggle' export const MobileIconsBar: React.FC = () => { const [, setMessages] = useUIState() @@ -42,9 +42,7 @@ export const MobileIconsBar: React.FC = () => { - + diff --git a/components/sensor-toggle.tsx b/components/sensor-toggle.tsx new file mode 100644 index 00000000..15b97bcf --- /dev/null +++ b/components/sensor-toggle.tsx @@ -0,0 +1,32 @@ +'use client' + +import * as React from 'react' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { TentTree, RadioTower } from 'lucide-react' +import { useMapToggle, MapToggleEnum } from './map-toggle-context' + +export function SensorToggle() { + const { setMapType } = useMapToggle(); + + return ( + + + + + + {setMapType(MapToggleEnum.SensorMode)}}> + + Sensors + + + + ) +} diff --git a/lib/sensors.ts b/lib/sensors.ts new file mode 100644 index 00000000..8b886cc9 --- /dev/null +++ b/lib/sensors.ts @@ -0,0 +1,56 @@ +export interface Sensor { + id: string; + title: string; + location: { + latitude: number; + longitude: number; + }; + images: { + current: { + preview: string; + }; + daylight: { + preview: string; + } + }; + urls: { + webcam: string; + }; +} + +export interface WindyResponse { + webcams: Sensor[]; +} + +export async function getSensors(): Promise { + const apiKey = process.env.WINDY_API_KEY; + if (!apiKey || apiKey === 'YOUR_WINDY_API_KEY') { + console.error('WINDY_API_KEY is not defined or not set in the environment variables.'); + throw new Error('WINDY_API_KEY is not defined or not set in the environment variables.'); + } + + try { + console.log('Fetching sensors with API key:', apiKey ? 'present' : 'missing'); + const response = await fetch('https://api.windy.com/webcams/api/v3/webcams?include=location,images,urls', { + method: 'GET', + headers: { + 'x-windy-api-key': apiKey, + }, + }); + + console.log('Windy API response status:', response.status); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Failed to fetch sensors:', response.statusText, errorText); + throw new Error(`Failed to fetch sensors: ${response.statusText}`); + } + + const data = await response.json(); + console.log('Successfully fetched sensor data:', data); + return data; + } catch (error) { + console.error('Error in getSensors:', error); + throw error; + } +}