Initial commit: breakpilot-lehrer - Lehrer KI Platform
Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website, Klausur-Service, School-Service, Voice-Service, Geo-Service, BreakPilot Drive, Agent-Core Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
501
studio-v2/app/geo-lernwelt/page.tsx
Normal file
501
studio-v2/app/geo-lernwelt/page.tsx
Normal file
@@ -0,0 +1,501 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import {
|
||||
AOIResponse,
|
||||
AOITheme,
|
||||
AOIQuality,
|
||||
Difficulty,
|
||||
GeoJSONPolygon,
|
||||
LearningNode,
|
||||
GeoServiceHealth,
|
||||
DemoTemplate,
|
||||
} from './types'
|
||||
|
||||
// Dynamic imports for map components (no SSR)
|
||||
const AOISelector = dynamic(
|
||||
() => import('@/components/geo-lernwelt/AOISelector'),
|
||||
{ ssr: false, loading: () => <MapLoadingPlaceholder /> }
|
||||
)
|
||||
|
||||
const UnityViewer = dynamic(
|
||||
() => import('@/components/geo-lernwelt/UnityViewer'),
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
// API base URL
|
||||
const GEO_SERVICE_URL = process.env.NEXT_PUBLIC_GEO_SERVICE_URL || 'http://localhost:8088'
|
||||
|
||||
// Loading placeholder for map
|
||||
function MapLoadingPlaceholder() {
|
||||
return (
|
||||
<div className="w-full h-[400px] bg-slate-800 rounded-xl flex items-center justify-center">
|
||||
<div className="text-white/60 flex flex-col items-center gap-2">
|
||||
<div className="w-8 h-8 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
<span>Karte wird geladen...</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Theme icons and colors
|
||||
const THEME_CONFIG: Record<AOITheme, { icon: string; color: string; label: string }> = {
|
||||
topographie: { icon: '🏔️', color: 'bg-amber-500', label: 'Topographie' },
|
||||
landnutzung: { icon: '🏘️', color: 'bg-green-500', label: 'Landnutzung' },
|
||||
orientierung: { icon: '🧭', color: 'bg-blue-500', label: 'Orientierung' },
|
||||
geologie: { icon: '🪨', color: 'bg-stone-500', label: 'Geologie' },
|
||||
hydrologie: { icon: '💧', color: 'bg-cyan-500', label: 'Hydrologie' },
|
||||
vegetation: { icon: '🌲', color: 'bg-emerald-500', label: 'Vegetation' },
|
||||
}
|
||||
|
||||
export default function GeoLernweltPage() {
|
||||
// State
|
||||
const [serviceHealth, setServiceHealth] = useState<GeoServiceHealth | null>(null)
|
||||
const [currentAOI, setCurrentAOI] = useState<AOIResponse | null>(null)
|
||||
const [drawnPolygon, setDrawnPolygon] = useState<GeoJSONPolygon | null>(null)
|
||||
const [selectedTheme, setSelectedTheme] = useState<AOITheme>('topographie')
|
||||
const [quality, setQuality] = useState<AOIQuality>('medium')
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>('mittel')
|
||||
const [learningNodes, setLearningNodes] = useState<LearningNode[]>([])
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'map' | 'unity'>('map')
|
||||
const [demoTemplate, setDemoTemplate] = useState<DemoTemplate | null>(null)
|
||||
|
||||
// Check service health on mount
|
||||
useEffect(() => {
|
||||
checkServiceHealth()
|
||||
loadMainauTemplate()
|
||||
}, [])
|
||||
|
||||
const checkServiceHealth = async () => {
|
||||
try {
|
||||
const res = await fetch(`${GEO_SERVICE_URL}/health`)
|
||||
if (res.ok) {
|
||||
const health = await res.json()
|
||||
setServiceHealth(health)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Service health check failed:', e)
|
||||
setServiceHealth(null)
|
||||
}
|
||||
}
|
||||
|
||||
const loadMainauTemplate = async () => {
|
||||
try {
|
||||
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/aoi/templates/mainau`)
|
||||
if (res.ok) {
|
||||
const template = await res.json()
|
||||
setDemoTemplate(template)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load Mainau template:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const handlePolygonDrawn = useCallback((polygon: GeoJSONPolygon) => {
|
||||
setDrawnPolygon(polygon)
|
||||
setError(null)
|
||||
}, [])
|
||||
|
||||
const handleCreateAOI = async () => {
|
||||
if (!drawnPolygon) {
|
||||
setError('Bitte zeichne zuerst ein Gebiet auf der Karte.')
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/aoi`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
polygon: drawnPolygon,
|
||||
theme: selectedTheme,
|
||||
quality,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json()
|
||||
throw new Error(errorData.detail || 'Fehler beim Erstellen des Gebiets')
|
||||
}
|
||||
|
||||
const aoi = await res.json()
|
||||
setCurrentAOI(aoi)
|
||||
|
||||
// Poll for completion
|
||||
pollAOIStatus(aoi.aoi_id)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const pollAOIStatus = async (aoiId: string) => {
|
||||
const poll = async () => {
|
||||
try {
|
||||
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/aoi/${aoiId}`)
|
||||
if (res.ok) {
|
||||
const aoi = await res.json()
|
||||
setCurrentAOI(aoi)
|
||||
|
||||
if (aoi.status === 'completed') {
|
||||
// Load learning nodes
|
||||
generateLearningNodes(aoiId)
|
||||
} else if (aoi.status === 'failed') {
|
||||
setError('Verarbeitung fehlgeschlagen')
|
||||
} else {
|
||||
// Continue polling
|
||||
setTimeout(poll, 2000)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Polling error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
poll()
|
||||
}
|
||||
|
||||
const generateLearningNodes = async (aoiId: string) => {
|
||||
try {
|
||||
const res = await fetch(`${GEO_SERVICE_URL}/api/v1/learning/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
aoi_id: aoiId,
|
||||
theme: selectedTheme,
|
||||
difficulty,
|
||||
node_count: 5,
|
||||
language: 'de',
|
||||
}),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setLearningNodes(data.nodes)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to generate learning nodes:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLoadDemo = () => {
|
||||
if (demoTemplate) {
|
||||
setDrawnPolygon(demoTemplate.polygon)
|
||||
setSelectedTheme(demoTemplate.suggested_themes[0] || 'topographie')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-indigo-950 to-slate-900">
|
||||
{/* Header */}
|
||||
<header className="border-b border-white/10 bg-black/20 backdrop-blur-lg">
|
||||
<div className="max-w-7xl mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-3xl">🌍</span>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-white">Geo-Lernwelt</h1>
|
||||
<p className="text-sm text-white/60">Interaktive Erdkunde-Lernplattform</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
serviceHealth?.status === 'healthy'
|
||||
? 'bg-green-500'
|
||||
: 'bg-yellow-500 animate-pulse'
|
||||
}`}
|
||||
/>
|
||||
<span className="text-sm text-white/60">
|
||||
{serviceHealth?.status === 'healthy' ? 'Verbunden' : 'Verbinde...'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="max-w-7xl mx-auto px-4 py-6">
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex gap-2 mb-6">
|
||||
<button
|
||||
onClick={() => setActiveTab('map')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all ${
|
||||
activeTab === 'map'
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-white/60 hover:text-white hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
🗺️ Gebiet waehlen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('unity')}
|
||||
disabled={!currentAOI || currentAOI.status !== 'completed'}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-all ${
|
||||
activeTab === 'unity'
|
||||
? 'bg-white/10 text-white'
|
||||
: 'text-white/60 hover:text-white hover:bg-white/5'
|
||||
} disabled:opacity-40 disabled:cursor-not-allowed`}
|
||||
>
|
||||
🎮 3D-Lernwelt
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error Message */}
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-500/20 border border-red-500/50 rounded-xl text-red-200">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'map' ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Map Section (2/3) */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
{/* Map Card */}
|
||||
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 overflow-hidden">
|
||||
<div className="p-4 border-b border-white/10">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-medium text-white">Gebiet auf der Karte waehlen</h2>
|
||||
{demoTemplate && (
|
||||
<button
|
||||
onClick={handleLoadDemo}
|
||||
className="px-3 py-1.5 text-sm bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 rounded-lg transition-colors"
|
||||
>
|
||||
📍 Demo: Insel Mainau
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-white/60 mt-1">
|
||||
Zeichne ein Polygon (max. 4 km²) um das gewuenschte Lerngebiet
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="h-[500px]">
|
||||
<AOISelector
|
||||
onPolygonDrawn={handlePolygonDrawn}
|
||||
initialPolygon={drawnPolygon}
|
||||
maxAreaKm2={4}
|
||||
geoServiceUrl={GEO_SERVICE_URL}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Attribution */}
|
||||
<div className="text-xs text-white/40 text-center">
|
||||
Kartendaten: © OpenStreetMap contributors (ODbL) | Hoehenmodell: © Copernicus DEM
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings Panel (1/3) */}
|
||||
<div className="space-y-4">
|
||||
{/* Theme Selection */}
|
||||
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
|
||||
<h3 className="text-white font-medium mb-3">Lernthema</h3>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{(Object.keys(THEME_CONFIG) as AOITheme[]).map((theme) => {
|
||||
const config = THEME_CONFIG[theme]
|
||||
return (
|
||||
<button
|
||||
key={theme}
|
||||
onClick={() => setSelectedTheme(theme)}
|
||||
className={`p-3 rounded-xl text-left transition-all ${
|
||||
selectedTheme === theme
|
||||
? 'bg-white/15 border border-white/30'
|
||||
: 'bg-white/5 border border-transparent hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
<span className="text-2xl">{config.icon}</span>
|
||||
<div className="text-sm text-white mt-1">{config.label}</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quality Selection */}
|
||||
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
|
||||
<h3 className="text-white font-medium mb-3">Qualitaet</h3>
|
||||
<div className="flex gap-2">
|
||||
{(['low', 'medium', 'high'] as AOIQuality[]).map((q) => (
|
||||
<button
|
||||
key={q}
|
||||
onClick={() => setQuality(q)}
|
||||
className={`flex-1 py-2 rounded-lg text-sm transition-all ${
|
||||
quality === q
|
||||
? 'bg-white/15 text-white border border-white/30'
|
||||
: 'bg-white/5 text-white/60 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
{q === 'low' ? 'Schnell' : q === 'medium' ? 'Standard' : 'Hoch'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Difficulty Selection */}
|
||||
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
|
||||
<h3 className="text-white font-medium mb-3">Schwierigkeitsgrad</h3>
|
||||
<div className="flex gap-2">
|
||||
{(['leicht', 'mittel', 'schwer'] as Difficulty[]).map((d) => (
|
||||
<button
|
||||
key={d}
|
||||
onClick={() => setDifficulty(d)}
|
||||
className={`flex-1 py-2 rounded-lg text-sm capitalize transition-all ${
|
||||
difficulty === d
|
||||
? 'bg-white/15 text-white border border-white/30'
|
||||
: 'bg-white/5 text-white/60 hover:bg-white/10'
|
||||
}`}
|
||||
>
|
||||
{d}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Area Info */}
|
||||
{drawnPolygon && (
|
||||
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
|
||||
<h3 className="text-white font-medium mb-2">Ausgewaehltes Gebiet</h3>
|
||||
<div className="text-sm text-white/60">
|
||||
<p>Polygon gezeichnet ✓</p>
|
||||
<p className="text-white/40 text-xs mt-1">
|
||||
Klicke "Lernwelt erstellen" um fortzufahren
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Create Button */}
|
||||
<button
|
||||
onClick={handleCreateAOI}
|
||||
disabled={!drawnPolygon || isLoading}
|
||||
className={`w-full py-4 rounded-xl font-medium text-lg transition-all ${
|
||||
drawnPolygon && !isLoading
|
||||
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white hover:from-blue-600 hover:to-purple-600'
|
||||
: 'bg-white/10 text-white/40 cursor-not-allowed'
|
||||
}`}
|
||||
>
|
||||
{isLoading ? (
|
||||
<span className="flex items-center justify-center gap-2">
|
||||
<span className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
Wird erstellt...
|
||||
</span>
|
||||
) : (
|
||||
'🚀 Lernwelt erstellen'
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* AOI Status */}
|
||||
{currentAOI && (
|
||||
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
|
||||
<h3 className="text-white font-medium mb-2">Status</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
currentAOI.status === 'completed'
|
||||
? 'bg-green-500'
|
||||
: currentAOI.status === 'failed'
|
||||
? 'bg-red-500'
|
||||
: 'bg-yellow-500 animate-pulse'
|
||||
}`}
|
||||
/>
|
||||
<span className="text-sm text-white/80 capitalize">
|
||||
{currentAOI.status === 'queued'
|
||||
? 'In Warteschlange...'
|
||||
: currentAOI.status === 'processing'
|
||||
? 'Wird verarbeitet...'
|
||||
: currentAOI.status === 'completed'
|
||||
? 'Fertig!'
|
||||
: 'Fehlgeschlagen'}
|
||||
</span>
|
||||
</div>
|
||||
{currentAOI.area_km2 > 0 && (
|
||||
<p className="text-xs text-white/50">
|
||||
Flaeche: {currentAOI.area_km2.toFixed(2)} km²
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Unity 3D Viewer Tab */
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 overflow-hidden">
|
||||
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium text-white">3D-Lernwelt</h2>
|
||||
<p className="text-sm text-white/60">
|
||||
Erkunde das Gebiet und bearbeite die Lernstationen
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-white/60">
|
||||
{learningNodes.length} Lernstationen
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-[600px]">
|
||||
{currentAOI && currentAOI.status === 'completed' ? (
|
||||
<UnityViewer
|
||||
aoiId={currentAOI.aoi_id}
|
||||
manifestUrl={currentAOI.manifest_url}
|
||||
learningNodes={learningNodes}
|
||||
geoServiceUrl={GEO_SERVICE_URL}
|
||||
/>
|
||||
) : (
|
||||
<div className="h-full flex items-center justify-center text-white/60">
|
||||
Erstelle zuerst ein Lerngebiet im Tab "Gebiet waehlen"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Learning Nodes List */}
|
||||
{learningNodes.length > 0 && (
|
||||
<div className="bg-white/5 backdrop-blur-lg rounded-2xl border border-white/10 p-4">
|
||||
<h3 className="text-white font-medium mb-3">Lernstationen</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{learningNodes.map((node, idx) => (
|
||||
<div
|
||||
key={node.id}
|
||||
className="p-3 bg-white/5 rounded-xl border border-white/10"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="w-6 h-6 bg-blue-500/30 rounded-full flex items-center justify-center text-xs text-white">
|
||||
{idx + 1}
|
||||
</span>
|
||||
<span className="text-white font-medium text-sm">{node.title}</span>
|
||||
</div>
|
||||
<p className="text-xs text-white/60 line-clamp-2">{node.question}</p>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="text-xs bg-white/10 px-2 py-0.5 rounded text-white/60">
|
||||
{node.points} Punkte
|
||||
</span>
|
||||
{node.approved && (
|
||||
<span className="text-xs text-green-400">✓ Freigegeben</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
282
studio-v2/app/geo-lernwelt/types.ts
Normal file
282
studio-v2/app/geo-lernwelt/types.ts
Normal file
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* GeoEdu Service - TypeScript Types
|
||||
* Types for the geography learning platform
|
||||
*/
|
||||
|
||||
// Geographic types
|
||||
export interface Position {
|
||||
latitude: number
|
||||
longitude: number
|
||||
altitude?: number
|
||||
}
|
||||
|
||||
export interface Bounds {
|
||||
west: number
|
||||
south: number
|
||||
east: number
|
||||
north: number
|
||||
}
|
||||
|
||||
export interface GeoJSONPolygon {
|
||||
type: 'Polygon'
|
||||
coordinates: number[][][]
|
||||
}
|
||||
|
||||
// AOI (Area of Interest) types
|
||||
export type AOIStatus = 'queued' | 'processing' | 'completed' | 'failed'
|
||||
export type AOIQuality = 'low' | 'medium' | 'high'
|
||||
export type AOITheme =
|
||||
| 'topographie'
|
||||
| 'landnutzung'
|
||||
| 'orientierung'
|
||||
| 'geologie'
|
||||
| 'hydrologie'
|
||||
| 'vegetation'
|
||||
|
||||
export interface AOIRequest {
|
||||
polygon: GeoJSONPolygon
|
||||
theme: AOITheme
|
||||
quality: AOIQuality
|
||||
}
|
||||
|
||||
export interface AOIResponse {
|
||||
aoi_id: string
|
||||
status: AOIStatus
|
||||
area_km2: number
|
||||
estimated_size_mb: number
|
||||
message?: string
|
||||
download_url?: string
|
||||
manifest_url?: string
|
||||
created_at?: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
export interface AOIManifest {
|
||||
version: string
|
||||
aoi_id: string
|
||||
created_at: string
|
||||
bounds: Bounds
|
||||
center: Position
|
||||
area_km2: number
|
||||
theme: AOITheme
|
||||
quality: AOIQuality
|
||||
assets: {
|
||||
terrain: { file: string; config: string }
|
||||
osm_features: { file: string }
|
||||
learning_positions: { file: string }
|
||||
attribution: { file: string }
|
||||
}
|
||||
unity: {
|
||||
coordinate_system: string
|
||||
scale: number
|
||||
terrain_resolution: number
|
||||
}
|
||||
}
|
||||
|
||||
// Learning Node types
|
||||
export type NodeType = 'question' | 'observation' | 'exploration'
|
||||
export type Difficulty = 'leicht' | 'mittel' | 'schwer'
|
||||
|
||||
export interface LearningNode {
|
||||
id: string
|
||||
aoi_id: string
|
||||
title: string
|
||||
theme: AOITheme
|
||||
position: Position
|
||||
question: string
|
||||
hints: string[]
|
||||
answer: string
|
||||
explanation: string
|
||||
node_type: NodeType
|
||||
points: number
|
||||
approved: boolean
|
||||
media?: {
|
||||
type: 'image' | 'audio' | 'video'
|
||||
url: string
|
||||
}[]
|
||||
tags?: string[]
|
||||
difficulty?: Difficulty
|
||||
grade_level?: string
|
||||
}
|
||||
|
||||
export interface LearningNodeRequest {
|
||||
aoi_id: string
|
||||
theme: AOITheme
|
||||
difficulty: Difficulty
|
||||
node_count: number
|
||||
grade_level?: string
|
||||
language?: string
|
||||
}
|
||||
|
||||
export interface LearningNodeResponse {
|
||||
aoi_id: string
|
||||
theme: string
|
||||
nodes: LearningNode[]
|
||||
total_count: number
|
||||
generation_model: string
|
||||
}
|
||||
|
||||
// Theme template types
|
||||
export interface ThemeTemplate {
|
||||
id: AOITheme
|
||||
name: string
|
||||
description: string
|
||||
icon: string
|
||||
grade_levels: string[]
|
||||
example_questions: string[]
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
export interface LearningTemplates {
|
||||
themes: ThemeTemplate[]
|
||||
difficulties: {
|
||||
id: Difficulty
|
||||
name: string
|
||||
description: string
|
||||
}[]
|
||||
supported_languages: string[]
|
||||
}
|
||||
|
||||
// Attribution types
|
||||
export interface AttributionSource {
|
||||
name: string
|
||||
license: string
|
||||
url: string
|
||||
attribution: string
|
||||
required: boolean
|
||||
logo_url?: string
|
||||
}
|
||||
|
||||
export interface Attribution {
|
||||
sources: AttributionSource[]
|
||||
generated_at: string
|
||||
notice: string
|
||||
}
|
||||
|
||||
// Tile metadata types
|
||||
export interface TileMetadata {
|
||||
name: string
|
||||
description: string
|
||||
format: string
|
||||
scheme: string
|
||||
minzoom: number
|
||||
maxzoom: number
|
||||
bounds: [number, number, number, number]
|
||||
center: [number, number, number]
|
||||
attribution: string
|
||||
data_available: boolean
|
||||
last_updated?: string
|
||||
}
|
||||
|
||||
export interface DEMMetadata {
|
||||
name: string
|
||||
description: string
|
||||
resolution_m: number
|
||||
coverage: string
|
||||
bounds: [number, number, number, number]
|
||||
vertical_datum: string
|
||||
horizontal_datum: string
|
||||
license: string
|
||||
attribution: string
|
||||
data_available: boolean
|
||||
tiles_generated: number
|
||||
}
|
||||
|
||||
// Service health status
|
||||
export interface GeoServiceHealth {
|
||||
status: 'healthy' | 'degraded' | 'unhealthy'
|
||||
service: string
|
||||
version: string
|
||||
environment: string
|
||||
data_status: {
|
||||
pmtiles_available: boolean
|
||||
dem_available: boolean
|
||||
tile_cache_dir: boolean
|
||||
bundle_dir: boolean
|
||||
}
|
||||
config: {
|
||||
max_aoi_size_km2: number
|
||||
supported_themes: AOITheme[]
|
||||
}
|
||||
}
|
||||
|
||||
// Map style types (for MapLibre)
|
||||
export interface MapStyle {
|
||||
version: number
|
||||
name: string
|
||||
metadata: {
|
||||
description: string
|
||||
attribution: string
|
||||
}
|
||||
sources: {
|
||||
[key: string]: {
|
||||
type: string
|
||||
tiles?: string[]
|
||||
url?: string
|
||||
minzoom?: number
|
||||
maxzoom?: number
|
||||
attribution?: string
|
||||
tileSize?: number
|
||||
}
|
||||
}
|
||||
layers: MapLayer[]
|
||||
terrain?: {
|
||||
source: string
|
||||
exaggeration: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface MapLayer {
|
||||
id: string
|
||||
type: string
|
||||
source?: string // Optional for background layers
|
||||
'source-layer'?: string
|
||||
minzoom?: number
|
||||
maxzoom?: number
|
||||
filter?: unknown[] // MapLibre filter expressions can have mixed types
|
||||
layout?: Record<string, unknown>
|
||||
paint?: Record<string, unknown>
|
||||
}
|
||||
|
||||
// UI State types
|
||||
export interface GeoLernweltState {
|
||||
// Current AOI
|
||||
currentAOI: AOIResponse | null
|
||||
drawnPolygon: GeoJSONPolygon | null
|
||||
|
||||
// Selected theme and settings
|
||||
selectedTheme: AOITheme
|
||||
quality: AOIQuality
|
||||
difficulty: Difficulty
|
||||
|
||||
// Learning nodes
|
||||
learningNodes: LearningNode[]
|
||||
selectedNode: LearningNode | null
|
||||
|
||||
// UI state
|
||||
isDrawing: boolean
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
|
||||
// Unity viewer state
|
||||
unityReady: boolean
|
||||
unityProgress: number
|
||||
}
|
||||
|
||||
// API response wrapper
|
||||
export interface ApiResponse<T> {
|
||||
data?: T
|
||||
error?: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
// Demo templates
|
||||
export interface DemoTemplate {
|
||||
name: string
|
||||
description: string
|
||||
polygon: GeoJSONPolygon
|
||||
center: [number, number]
|
||||
area_km2: number
|
||||
suggested_themes: AOITheme[]
|
||||
features: string[]
|
||||
}
|
||||
Reference in New Issue
Block a user