mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
Refactor MapNext
to GeoMap
, streamline code, and integrate optimized GeoJSON-based port rendering.
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -9426,6 +9426,7 @@ checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7"
|
||||
name = "yachtpit"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ais",
|
||||
"anyhow",
|
||||
"base-map",
|
||||
"bevy",
|
||||
|
@@ -2,7 +2,7 @@ import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import {Box, Button, HStack, Text} from '@chakra-ui/react';
|
||||
import {useColorMode} from './components/ui/color-mode';
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
import MapNext from "@/MapNext.tsx";
|
||||
import GeoMap from "@/GeoMap";
|
||||
import {getNeumorphicColors, getNeumorphicStyle} from './theme/neumorphic-theme';
|
||||
import {layers, LayerSelector} from "@/LayerSelector.tsx";
|
||||
import {useAISProvider, type VesselData} from './ais-provider';
|
||||
@@ -373,7 +373,7 @@ function App() {
|
||||
</Button>
|
||||
<LayerSelector onClick={handleLayerChange}/>
|
||||
</HStack>
|
||||
<MapNext
|
||||
<GeoMap
|
||||
mapboxPublicKey={atob(key)}
|
||||
vesselPosition={vesselPosition}
|
||||
layer={selectedLayer}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import type {Geolocation} from "@/MapNext.tsx";
|
||||
import type {Geolocation} from "@/Map.tsx";
|
||||
|
||||
|
||||
export class NativeGeolocation implements Geolocation {
|
||||
|
96
crates/base-map/map/src/GeoMap.tsx
Normal file
96
crates/base-map/map/src/GeoMap.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import {useMemo, useCallback, useRef} from 'react';
|
||||
import Map, {
|
||||
Source, Layer,
|
||||
NavigationControl, FullscreenControl, ScaleControl, GeolocateControl,
|
||||
type MapRef
|
||||
} from 'react-map-gl/mapbox';
|
||||
import {Box} from '@chakra-ui/react';
|
||||
import type {Feature, FeatureCollection, Point} from 'geojson';
|
||||
import PORTS from './test_data/nautical-base-data.json';
|
||||
import type {VesselData} from './ais-provider';
|
||||
|
||||
export interface Geolocation {
|
||||
clearWatch(watchId: number): void;
|
||||
getCurrentPosition(
|
||||
successCallback: PositionCallback,
|
||||
errorCallback?: PositionErrorCallback | null,
|
||||
options?: PositionOptions
|
||||
): void;
|
||||
watchPosition(
|
||||
successCallback: PositionCallback,
|
||||
errorCallback?: PositionErrorCallback | null,
|
||||
options?: PositionOptions
|
||||
): number;
|
||||
}
|
||||
|
||||
interface MapNextProps {
|
||||
mapboxPublicKey: string;
|
||||
geolocation: Geolocation;
|
||||
vesselPosition?: any;
|
||||
layer?: any;
|
||||
mapView?: any;
|
||||
aisVessels?: VesselData[];
|
||||
onVesselClick?: (vessel: VesselData) => void;
|
||||
vesselPopup?: VesselData | null;
|
||||
onVesselPopupClose?: () => void;
|
||||
}
|
||||
|
||||
export default function GeoMap(props: MapNextProps) {
|
||||
const mapRef = useRef<MapRef | null>(null);
|
||||
|
||||
const portsGeoJSON = useMemo<FeatureCollection<Point>>(() => ({
|
||||
type: 'FeatureCollection',
|
||||
features: PORTS.map(port => ({
|
||||
type: 'Feature',
|
||||
geometry: {type: 'Point', coordinates: [port.longitude, port.latitude]},
|
||||
properties: {city: port.city, state: port.state}
|
||||
} as Feature<Point>))
|
||||
}), []);
|
||||
|
||||
const handleGeolocate = useCallback((pos: GeolocationPosition) => {
|
||||
console.log('User location loaded:', pos);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Map
|
||||
ref={mapRef}
|
||||
initialViewState={{
|
||||
latitude: props.mapView?.latitude ?? 40,
|
||||
longitude: props.mapView?.longitude ?? -100,
|
||||
zoom: props.mapView?.zoom ?? 3.5,
|
||||
bearing: 0,
|
||||
pitch: 0
|
||||
}}
|
||||
key={`${props.mapView?.latitude}-${props.mapView?.longitude}-${props.mapView?.zoom}`}
|
||||
mapStyle={props.layer?.value ?? 'mapbox://styles/mapbox/standard'}
|
||||
mapboxAccessToken={props.mapboxPublicKey}
|
||||
style={{ position: 'fixed', inset: 0 }}
|
||||
>
|
||||
<Source id="ports" type="geojson" data={portsGeoJSON}>
|
||||
<Layer
|
||||
id="ports-layer"
|
||||
type="circle"
|
||||
paint={{
|
||||
'circle-radius': 6,
|
||||
'circle-color': '#007bff',
|
||||
'circle-stroke-width': 1,
|
||||
'circle-stroke-color': '#ffffff'
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
|
||||
<GeolocateControl
|
||||
showUserHeading={true}
|
||||
showUserLocation={true}
|
||||
geolocation={props.geolocation}
|
||||
position="top-left"
|
||||
onGeolocate={handleGeolocate}
|
||||
/>
|
||||
<FullscreenControl position="top-left" />
|
||||
<NavigationControl position="top-left" />
|
||||
<ScaleControl />
|
||||
</Map>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -1,238 +0,0 @@
|
||||
import {useState, useMemo, useCallback, useRef} from 'react';
|
||||
import Map, {
|
||||
Marker,
|
||||
Popup,
|
||||
NavigationControl,
|
||||
FullscreenControl,
|
||||
ScaleControl,
|
||||
GeolocateControl,
|
||||
type MapRef
|
||||
} from 'react-map-gl/mapbox';
|
||||
|
||||
import ControlPanel from './control-panel.tsx';
|
||||
import Pin from './pin.tsx';
|
||||
import VesselMarker from './vessel-marker';
|
||||
import type { VesselData } from './ais-provider';
|
||||
|
||||
import PORTS from './test_data/nautical-base-data.json';
|
||||
import {Box} from "@chakra-ui/react";
|
||||
|
||||
|
||||
export interface Geolocation {
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Geolocation/clearWatch) */
|
||||
clearWatch(watchId: number): void;
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Geolocation/getCurrentPosition) */
|
||||
getCurrentPosition(successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions): void;
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Geolocation/watchPosition) */
|
||||
watchPosition(successCallback: PositionCallback, errorCallback?: PositionErrorCallback | null, options?: PositionOptions): number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
interface MapNextProps {
|
||||
mapboxPublicKey: string;
|
||||
geolocation: Geolocation;
|
||||
vesselPosition?: any;
|
||||
layer?: any;
|
||||
mapView?: any;
|
||||
aisVessels?: VesselData[];
|
||||
onVesselClick?: (vessel: VesselData) => void;
|
||||
vesselPopup?: VesselData | null;
|
||||
onVesselPopupClose?: () => void;
|
||||
}
|
||||
|
||||
export default function MapNext(props: MapNextProps) {
|
||||
const [popupInfo, setPopupInfo] = useState(null);
|
||||
const mapRef = useRef<MapRef | null>(null);
|
||||
|
||||
// Handle user location events
|
||||
const handleGeolocate = useCallback((position: GeolocationPosition) => {
|
||||
console.log('User location loaded:', position);
|
||||
}, []);
|
||||
|
||||
const handleTrackUserLocationStart = useCallback(() => {
|
||||
console.log('Started tracking user location');
|
||||
}, []);
|
||||
|
||||
const handleTrackUserLocationEnd = useCallback(() => {
|
||||
console.log('Stopped tracking user location');
|
||||
}, []);
|
||||
|
||||
const pins = useMemo(
|
||||
() =>
|
||||
PORTS.map((city, index) => (
|
||||
<Marker
|
||||
key={`marker-${index}`}
|
||||
longitude={city.longitude}
|
||||
latitude={city.latitude}
|
||||
anchor="bottom"
|
||||
onClick={e => {
|
||||
// If we let the click event propagates to the map, it will immediately close the popup
|
||||
// with `closeOnClick: true`
|
||||
e.originalEvent.stopPropagation();
|
||||
/*
|
||||
src/MapNext.tsx:34:38 - error TS2345: Argument of type '{ city: string; population: string; image: string; state: string; latitude: number; longitude: number; }' is not assignable to parameter of type 'SetStateAction<null>'.
|
||||
Type '{ city: string; population: string; image: string; state: string; latitude: number; longitude: number; }' provides no match for the signature '(prevState: null): null'.
|
||||
*/
|
||||
// @ts-ignore
|
||||
setPopupInfo(city);
|
||||
}}
|
||||
>
|
||||
<Pin />
|
||||
</Marker>
|
||||
)),
|
||||
[]
|
||||
);
|
||||
|
||||
// Helper function to get vessel color based on type
|
||||
const getVesselColor = (type: string): string => {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'yacht':
|
||||
case 'pleasure craft':
|
||||
return '#00cc66';
|
||||
case 'fishing vessel':
|
||||
case 'fishing':
|
||||
return '#ff6600';
|
||||
case 'cargo':
|
||||
case 'container':
|
||||
return '#cc0066';
|
||||
case 'tanker':
|
||||
return '#ff0000';
|
||||
case 'passenger':
|
||||
return '#6600cc';
|
||||
default:
|
||||
return '#0066cc';
|
||||
}
|
||||
};
|
||||
|
||||
// Create vessel markers
|
||||
const vesselMarkers = useMemo(() =>
|
||||
(props.aisVessels || []).map((vessel) => (
|
||||
<Marker
|
||||
key={`vessel-${vessel.id}`}
|
||||
longitude={vessel.longitude}
|
||||
latitude={vessel.latitude}
|
||||
anchor="center"
|
||||
onClick={(e) => {
|
||||
e.originalEvent.stopPropagation();
|
||||
if (props.onVesselClick) {
|
||||
props.onVesselClick(vessel);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<VesselMarker
|
||||
heading={vessel.heading}
|
||||
color={getVesselColor(vessel.type)}
|
||||
size={16}
|
||||
/>
|
||||
</Marker>
|
||||
)),
|
||||
[props.aisVessels, props.onVesselClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Map
|
||||
ref={mapRef}
|
||||
initialViewState={{
|
||||
latitude: props.mapView?.latitude || 40,
|
||||
longitude: props.mapView?.longitude || -100,
|
||||
zoom: props.mapView?.zoom || 3.5,
|
||||
bearing: 0,
|
||||
pitch: 0
|
||||
}}
|
||||
key={`${props.mapView?.latitude}-${props.mapView?.longitude}-${props.mapView?.zoom}`}
|
||||
mapStyle={props.layer?.value || "mapbox://styles/mapbox/standard"}
|
||||
mapboxAccessToken={props.mapboxPublicKey}
|
||||
style={{position: "fixed", width: '100%', height: '100%', bottom: 0, top: 0, left: 0, right: 0}}
|
||||
>
|
||||
<GeolocateControl
|
||||
showUserHeading={true}
|
||||
showUserLocation={true}
|
||||
geolocation={props.geolocation}
|
||||
position="top-left"
|
||||
onGeolocate={handleGeolocate}
|
||||
onTrackUserLocationStart={handleTrackUserLocationStart}
|
||||
onTrackUserLocationEnd={handleTrackUserLocationEnd}
|
||||
/>
|
||||
<FullscreenControl position="top-left" />
|
||||
<NavigationControl position="top-left" />
|
||||
<ScaleControl />
|
||||
|
||||
{pins}
|
||||
{vesselMarkers}
|
||||
|
||||
{/* Vessel Popup */}
|
||||
{props.vesselPopup && (
|
||||
<Popup
|
||||
longitude={props.vesselPopup.longitude}
|
||||
latitude={props.vesselPopup.latitude}
|
||||
anchor="bottom"
|
||||
onClose={() => props.onVesselPopupClose && props.onVesselPopupClose()}
|
||||
closeButton={true}
|
||||
closeOnClick={false}
|
||||
>
|
||||
<div style={{ padding: '10px', minWidth: '200px' }}>
|
||||
<h4 style={{ margin: '0 0 10px 0' }}>{props.vesselPopup.name}</h4>
|
||||
<div><strong>MMSI:</strong> {props.vesselPopup.mmsi}</div>
|
||||
<div><strong>Type:</strong> {props.vesselPopup.type}</div>
|
||||
<div><strong>Speed:</strong> {props.vesselPopup.speed.toFixed(1)} knots</div>
|
||||
<div><strong>Heading:</strong> {props.vesselPopup.heading}°</div>
|
||||
<div><strong>Position:</strong> {props.vesselPopup.latitude.toFixed(4)}, {props.vesselPopup.longitude.toFixed(4)}</div>
|
||||
<div style={{ fontSize: '12px', color: '#666', marginTop: '5px' }}>
|
||||
Last update: {props.vesselPopup.lastUpdate.toLocaleTimeString()}
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
)}
|
||||
|
||||
{popupInfo && (
|
||||
<Popup
|
||||
anchor="top"
|
||||
/*
|
||||
src/MapNext.tsx:66:53 - error TS2339: Property 'longitude' does not exist on type 'never'.
|
||||
|
||||
66 longitude={Number(popupInfo.longitude)}
|
||||
*/
|
||||
// @ts-ignore
|
||||
longitude={Number(popupInfo.longitude)}
|
||||
/*
|
||||
src/MapNext.tsx:67:52 - error TS2339: Property 'latitude' does not exist on type 'never'.
|
||||
|
||||
67 latitude={Number(popupInfo.latitude)}
|
||||
~~~~~~~~
|
||||
*/
|
||||
// @ts-ignore
|
||||
latitude={Number(popupInfo.latitude)}
|
||||
onClose={() => setPopupInfo(null)}
|
||||
>
|
||||
<div>
|
||||
{/*src/MapNext.tsx:71:40 - error TS2339: Property 'city' does not exist on type 'never'.
|
||||
|
||||
71 {popupInfo.city}, {popupInfo.state} |{' '}
|
||||
~~~~*/}
|
||||
|
||||
{/*@ts-ignore*/}{/*@ts-ignore*/}
|
||||
{popupInfo.city},{popupInfo.state}
|
||||
{/*@ts-ignore*/}
|
||||
<a
|
||||
target="_new"
|
||||
|
||||
href={`http://en.wikipedia.org/w/index.php?title=Special:Search&search=${(popupInfo as any).city}, ${(popupInfo as any).state}`}
|
||||
>
|
||||
Wikipedia
|
||||
</a>
|
||||
</div>
|
||||
{/*@ts-ignore*/}
|
||||
<img width="100%" src={popupInfo.image} />
|
||||
</Popup>
|
||||
)}
|
||||
|
||||
|
||||
|
||||
</Map>
|
||||
|
||||
<ControlPanel />
|
||||
</Box>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user