feat: complete dashboard redesign, proxy unification, and Windows compatibility fixes
This commit is contained in:
@@ -11,6 +11,8 @@ export default function NavigationMap() {
|
||||
const shipMarkerRef = useRef(null)
|
||||
const trackSourceRef = useRef(false)
|
||||
const [zoom, setZoom] = useState(11)
|
||||
const [mapError, setMapError] = useState(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const api = getApi()
|
||||
const snapshot = api.signalk.getSnapshot?.()
|
||||
@@ -22,7 +24,9 @@ export default function NavigationMap() {
|
||||
useEffect(() => {
|
||||
if (map.current) return
|
||||
|
||||
map.current = new maplibregl.Map({
|
||||
try {
|
||||
setIsLoading(true)
|
||||
map.current = new maplibregl.Map({
|
||||
container: mapContainer.current,
|
||||
style: {
|
||||
version: 8,
|
||||
@@ -177,9 +181,28 @@ export default function NavigationMap() {
|
||||
})
|
||||
|
||||
map.current.on('zoom', () => setZoom(map.current.getZoom()))
|
||||
|
||||
map.current.on('load', () => {
|
||||
setIsLoading(false)
|
||||
setMapError(null)
|
||||
})
|
||||
|
||||
map.current.on('error', (e) => {
|
||||
console.error('Map error:', e)
|
||||
setMapError('Map failed to load. Check your internet connection.')
|
||||
setIsLoading(false)
|
||||
})
|
||||
|
||||
return () => {
|
||||
// Cleanup is handled by React
|
||||
if (map.current) {
|
||||
map.current.remove()
|
||||
map.current = null
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize map:', error)
|
||||
setMapError(`Map initialization failed: ${error.message}`)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -191,11 +214,14 @@ export default function NavigationMap() {
|
||||
const trackSource = map.current.getSource('track')
|
||||
if (trackSource && trackSourceRef.current) {
|
||||
const currentData = trackSource._data
|
||||
if (currentData.geometry.coordinates.length > 500) {
|
||||
currentData.geometry.coordinates.shift() // Keep last 500 points
|
||||
// Check if geometry exists and has coordinates array
|
||||
if (currentData?.geometry?.coordinates) {
|
||||
if (currentData.geometry.coordinates.length > 500) {
|
||||
currentData.geometry.coordinates.shift() // Keep last 500 points
|
||||
}
|
||||
currentData.geometry.coordinates.push([lon, lat])
|
||||
trackSource.setData(currentData)
|
||||
}
|
||||
currentData.geometry.coordinates.push([lon, lat])
|
||||
trackSource.setData(currentData)
|
||||
}
|
||||
|
||||
map.current.getSource('ship').setData({
|
||||
@@ -214,8 +240,34 @@ export default function NavigationMap() {
|
||||
}
|
||||
}, [lat, lon, heading, zoom])
|
||||
|
||||
// Show error state
|
||||
if (mapError) {
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
<div style={styles.errorState}>
|
||||
<div style={styles.errorIcon}>⚠️</div>
|
||||
<div style={styles.errorTitle}>Karte konnte nicht geladen werden</div>
|
||||
<div style={styles.errorMessage}>{mapError}</div>
|
||||
<button
|
||||
className="primary"
|
||||
onClick={() => window.location.reload()}
|
||||
style={{ marginTop: 16 }}
|
||||
>
|
||||
Neu laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{isLoading && (
|
||||
<div style={styles.loadingOverlay}>
|
||||
<div style={styles.spinner}></div>
|
||||
<div style={{ marginTop: 12, color: 'var(--muted)' }}>Karte wird geladen...</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={mapContainer} style={styles.mapBox} />
|
||||
|
||||
{/* Map Controls */}
|
||||
@@ -384,4 +436,51 @@ const styles = {
|
||||
padding: '4px 8px',
|
||||
zIndex: 500,
|
||||
},
|
||||
loadingOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'var(--surface)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 2000,
|
||||
},
|
||||
spinner: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
border: '4px solid var(--border)',
|
||||
borderTop: '4px solid var(--accent)',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
},
|
||||
errorState: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
padding: 32,
|
||||
textAlign: 'center',
|
||||
background: 'var(--surface)',
|
||||
borderRadius: 'var(--radius)',
|
||||
},
|
||||
errorIcon: {
|
||||
fontSize: 48,
|
||||
marginBottom: 16,
|
||||
},
|
||||
errorTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 600,
|
||||
color: 'var(--text)',
|
||||
marginBottom: 8,
|
||||
},
|
||||
errorMessage: {
|
||||
fontSize: 14,
|
||||
color: 'var(--muted)',
|
||||
maxWidth: 400,
|
||||
},
|
||||
}
|
||||
|
||||
204
dashboard/src/components/nav/SimpleNavigationMap.jsx
Normal file
204
dashboard/src/components/nav/SimpleNavigationMap.jsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import { useNMEA } from '../../hooks/useNMEA.js'
|
||||
|
||||
// Simple fallback map component without MapLibre (no external dependencies)
|
||||
export default function SimpleNavigationMap() {
|
||||
const { lat, lon, heading, sog, cog } = useNMEA()
|
||||
|
||||
const mapLat = lat ?? 54.32
|
||||
const mapLon = lon ?? 10.22
|
||||
|
||||
// Simple marker rotation based on heading
|
||||
const rotation = heading ?? cog ?? 0
|
||||
|
||||
return (
|
||||
<div style={styles.container}>
|
||||
{/* Info Panel */}
|
||||
<div style={styles.infoPanel}>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.label}>Position</span>
|
||||
<span style={styles.value}>
|
||||
{lat != null ? lat.toFixed(5) : '--'}° N
|
||||
</span>
|
||||
</div>
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.label}></span>
|
||||
<span style={styles.value}>
|
||||
{lon != null ? lon.toFixed(5) : '--'}° E
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{heading != null && (
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.label}>Heading</span>
|
||||
<span style={styles.value}>{Math.round(heading)}°</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sog != null && (
|
||||
<div style={styles.infoRow}>
|
||||
<span style={styles.label}>Speed</span>
|
||||
<span style={styles.value}>{(sog * 1.943844).toFixed(1)} kn</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Simple visual representation */}
|
||||
<div style={styles.mapView}>
|
||||
<div style={styles.compassRose}>
|
||||
<div style={styles.northMarker}>N</div>
|
||||
<div
|
||||
style={{
|
||||
...styles.shipMarker,
|
||||
transform: `rotate(${rotation}deg)`
|
||||
}}
|
||||
>
|
||||
▲
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={styles.coords}>
|
||||
{lat != null && lon != null ? (
|
||||
<>
|
||||
<div>{lat.toFixed(5)}°</div>
|
||||
<div>{lon.toFixed(5)}°</div>
|
||||
</>
|
||||
) : (
|
||||
<div style={{ color: 'var(--muted)' }}>No GPS signal</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Map notice */}
|
||||
<div style={styles.notice}>
|
||||
<div style={styles.noticeTitle}>🗺️ Karten-Modus</div>
|
||||
<div style={styles.noticeText}>
|
||||
Interaktive Seekarte ist im Production Mode verfügbar.
|
||||
<br />
|
||||
Hier siehst du Position, Kurs und Geschwindigkeit.
|
||||
</div>
|
||||
<a
|
||||
href="http://localhost:3000"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style={styles.link}
|
||||
>
|
||||
→ SignalK Karte öffnen (Port 3000)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
container: {
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
background: 'var(--surface)',
|
||||
borderRadius: 'var(--radius)',
|
||||
border: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden',
|
||||
minHeight: 400,
|
||||
},
|
||||
mapView: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 32,
|
||||
padding: 32,
|
||||
},
|
||||
compassRose: {
|
||||
position: 'relative',
|
||||
width: 180,
|
||||
height: 180,
|
||||
border: '2px solid var(--accent)',
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'radial-gradient(circle, rgba(14, 165, 233, 0.05) 0%, transparent 70%)',
|
||||
},
|
||||
northMarker: {
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
fontSize: 14,
|
||||
fontWeight: 700,
|
||||
color: 'var(--accent)',
|
||||
},
|
||||
shipMarker: {
|
||||
fontSize: 48,
|
||||
color: 'var(--accent)',
|
||||
transition: 'transform 0.5s ease-out',
|
||||
},
|
||||
coords: {
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: 16,
|
||||
fontWeight: 600,
|
||||
color: 'var(--text)',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.5,
|
||||
},
|
||||
infoPanel: {
|
||||
background: 'var(--glass-bg)',
|
||||
backdropFilter: 'blur(var(--glass-blur))',
|
||||
WebkitBackdropFilter: 'blur(var(--glass-blur))',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 'var(--radius-lg)',
|
||||
padding: '12px 16px',
|
||||
margin: 16,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 8,
|
||||
},
|
||||
infoRow: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 16,
|
||||
},
|
||||
label: {
|
||||
color: 'var(--muted)',
|
||||
fontWeight: 600,
|
||||
fontSize: 11,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
},
|
||||
value: {
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontWeight: 600,
|
||||
color: 'var(--accent)',
|
||||
fontSize: 13,
|
||||
},
|
||||
notice: {
|
||||
background: 'var(--surface2)',
|
||||
padding: 20,
|
||||
borderTop: '1px solid var(--border)',
|
||||
textAlign: 'center',
|
||||
},
|
||||
noticeTitle: {
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: 'var(--text)',
|
||||
marginBottom: 8,
|
||||
},
|
||||
noticeText: {
|
||||
fontSize: 12,
|
||||
color: 'var(--muted)',
|
||||
lineHeight: 1.5,
|
||||
marginBottom: 12,
|
||||
},
|
||||
link: {
|
||||
display: 'inline-block',
|
||||
fontSize: 12,
|
||||
color: 'var(--accent)',
|
||||
textDecoration: 'none',
|
||||
fontWeight: 600,
|
||||
padding: '6px 12px',
|
||||
borderRadius: 'var(--radius)',
|
||||
background: 'rgba(14, 165, 233, 0.1)',
|
||||
border: '1px solid rgba(14, 165, 233, 0.3)',
|
||||
transition: 'all 0.2s',
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user