Skip to content

Commit

Permalink
Merge pull request #12 from P7-AIS/9-handle-streaming
Browse files Browse the repository at this point in the history
9 handle streaming
  • Loading branch information
AndersToft20 authored Oct 25, 2024
2 parents 5a239fc + 0af4231 commit e50cd85
Show file tree
Hide file tree
Showing 21 changed files with 330 additions and 160 deletions.
2 changes: 1 addition & 1 deletion AIS-protobuf
Submodule AIS-protobuf updated 1 files
+19 −0 ais.proto
Binary file added FaviconAIS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 5 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Gabarito:[email protected]&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
<link rel="icon" href="/FaviconAIS.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>AIS Analyzer</title>
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/map.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'
import { MapContainer, TileLayer, useMap } from 'react-leaflet'

import L from 'leaflet'

Expand Down
2 changes: 1 addition & 1 deletion src/components/monitoringMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default function MonitoringMenu({ monitoredVessels, children }: IMonitori
const [isCollapsed, setIsCollapsed] = useState<boolean>(true)

return (
<div className="flex flex-col h-full rounded-lg border-neural_3 border-2 bg-neutral_2 px-2">
<div className="flex flex-col h-full rounded-lg border-2 bg-neutral_2 px-2">
<div className={`flex flex-row justify-between items-center ${!isCollapsed && 'border-b-2'} gap-4 p-2`}>
<h1 className="text-xl font-bold">Monitoring overview</h1>
<p className="text-sm">{monitoredVessels.length} ships</p>
Expand Down
44 changes: 35 additions & 9 deletions src/components/popup.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,44 @@
import { useEffect, useState } from 'react'
import { useAppContext } from '../contexts/appcontext'
import { IDetailedVessel } from '../models/detailedVessel'

interface IPopupProps {
vessel: IDetailedVessel
mmsi: number
}

export default function Popup({ vessel }: IPopupProps) {
export default function Popup({ mmsi }: IPopupProps) {
const { clientHandler, myDateTime } = useAppContext()
const [vesselDetails, setVesselDetails] = useState<IDetailedVessel | undefined>(undefined)
const [loading, setLoading] = useState(true)

useEffect(() => {
const fetchDetails = async () => {
const details = await clientHandler.getVesselInfo({ mmsi, timestamp: myDateTime.getTime() })
setVesselDetails(details)
}

fetchDetails()
setLoading(false)
}, [])

return (
<div>
<h2>ID: {vessel.id}</h2>
<p>Name: {vessel.name}</p>
<p>Callsign: {vessel.callSign}</p>
<p>Length: {vessel.length}</p>
<p>pos fixing device: {vessel.positionFixingDevice}</p>
<p>MMSI: {vessel.mmsi}</p>
<div id="popup-container" className="h-[300px] w-[180px]">
{loading ? (
<p>Loading...</p>
) : (
vesselDetails && (
<>
<p>Name: {vesselDetails.name}</p>
<p>MMSI: {vesselDetails.mmsi}</p>
<p>IMO: {vesselDetails.imo}</p>
<p>Ship type: {vesselDetails.shipType}</p>
<p>Width: {vesselDetails.width}</p>
<p>Length: {vesselDetails.length}</p>
<p>Callsign: {vesselDetails.callSign}</p>
<p>Pos fixing device: {vesselDetails.positionFixingDevice}</p>
</>
)
)}
</div>
)
}
88 changes: 48 additions & 40 deletions src/components/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export default function Toolbar({ map, onMonitoringAreaChange }: IToolbarProps)

const clearTool = useCallback(() => {
if (map !== null) {
onMonitoringAreaChange(undefined) //no monitoring area active
map.eachLayer(function (layer: L.Layer) {
if (!(layer instanceof L.TileLayer || layer instanceof L.Marker)) {
map.removeLayer(layer)
Expand All @@ -23,10 +22,15 @@ export default function Toolbar({ map, onMonitoringAreaChange }: IToolbarProps)
}
}, [map, onMonitoringAreaChange])

function clearOnClick() {
clearTool()
onMonitoringAreaChange(undefined)
}

useEffect(() => {
if (activeTool !== ActiveGuiTool.Mouse) {
clearTool()
map.pm.enableDraw(activeTool, { snappable: true })
map.pm.enableDraw(activeTool, { snappable: false })
}
}, [activeTool, clearTool, map.pm])

Expand Down Expand Up @@ -54,45 +58,49 @@ export default function Toolbar({ map, onMonitoringAreaChange }: IToolbarProps)
}, [map, setActiveTool, onMonitoringAreaChange])

return (
<span className="inline-flex flex-col space-y-1">
<button onClick={() => setActiveTool(ActiveGuiTool.Rectangle)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
className="bi bi-square"
viewBox="0 0 16 16"
>
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z" />
</svg>
</button>
<div className="flex flex-col gap-4 bg-gray-700 text-gray-300 rounded-lg p-4">
<p className="text-white">Focus area tools</p>

<div id="tools" className="flex gap-4 items-center">
<button title="Draw monitoring area as rectangle" className="hover:text-gray-100" onClick={() => setActiveTool(ActiveGuiTool.Rectangle)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
className="bi bi-square"
viewBox="0 0 16 16"
>
<path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z" />
</svg>
</button>

<button onClick={() => setActiveTool(ActiveGuiTool.Polygon)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
className="bi bi-heptagon"
viewBox="0 0 16 16"
>
<path d="M7.779.052a.5.5 0 0 1 .442 0l6.015 2.97a.5.5 0 0 1 .267.34l1.485 6.676a.5.5 0 0 1-.093.415l-4.162 5.354a.5.5 0 0 1-.395.193H4.662a.5.5 0 0 1-.395-.193L.105 10.453a.5.5 0 0 1-.093-.415l1.485-6.676a.5.5 0 0 1 .267-.34zM2.422 3.813l-1.383 6.212L4.907 15h6.186l3.868-4.975-1.383-6.212L8 1.058z" />
</svg>
</button>
<button title="Draw monitoring area as polygon" className="hover:text-gray-100" onClick={() => setActiveTool(ActiveGuiTool.Polygon)}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
className="bi bi-heptagon"
viewBox="0 0 16 16"
>
<path d="M7.779.052a.5.5 0 0 1 .442 0l6.015 2.97a.5.5 0 0 1 .267.34l1.485 6.676a.5.5 0 0 1-.093.415l-4.162 5.354a.5.5 0 0 1-.395.193H4.662a.5.5 0 0 1-.395-.193L.105 10.453a.5.5 0 0 1-.093-.415l1.485-6.676a.5.5 0 0 1 .267-.34zM2.422 3.813l-1.383 6.212L4.907 15h6.186l3.868-4.975-1.383-6.212L8 1.058z" />
</svg>
</button>

<button className="bi bi-eraser" onClick={clearTool}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
className="bi bi-eraser"
viewBox="0 0 16 16"
>
<path d="M8.086 2.207a2 2 0 0 1 2.828 0l3.879 3.879a2 2 0 0 1 0 2.828l-5.5 5.5A2 2 0 0 1 7.879 15H5.12a2 2 0 0 1-1.414-.586l-2.5-2.5a2 2 0 0 1 0-2.828zm2.121.707a1 1 0 0 0-1.414 0L4.16 7.547l5.293 5.293 4.633-4.633a1 1 0 0 0 0-1.414zM8.746 13.547 3.453 8.254 1.914 9.793a1 1 0 0 0 0 1.414l2.5 2.5a1 1 0 0 0 .707.293H7.88a1 1 0 0 0 .707-.293z" />
</svg>
</button>
</span>
<button title="Clear monitoring area" className="bi bi-eraser hover:text-gray-100" onClick={clearOnClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="32"
height="32"
fill="currentColor"
className="bi bi-eraser"
viewBox="0 0 16 16"
>
<path d="M8.086 2.207a2 2 0 0 1 2.828 0l3.879 3.879a2 2 0 0 1 0 2.828l-5.5 5.5A2 2 0 0 1 7.879 15H5.12a2 2 0 0 1-1.414-.586l-2.5-2.5a2 2 0 0 1 0-2.828zm2.121.707a1 1 0 0 0-1.414 0L4.16 7.547l5.293 5.293 4.633-4.633a1 1 0 0 0 0-1.414zM8.746 13.547 3.453 8.254 1.914 9.793a1 1 0 0 0 0 1.414l2.5 2.5a1 1 0 0 0 .707.293H7.88a1 1 0 0 0 .707-.293z" />
</svg>
</button>
</div>
</div>
)
}
38 changes: 21 additions & 17 deletions src/components/vessel.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import { ISimpleVessel } from '../models/simpleVessel'
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { ILocation } from '../models/location'
import { IDetailedVessel } from '../models/detailedVessel'
import Popup from './popup'
import Path from './path'
import { Marker } from 'react-leaflet'
import { useAppContext } from '../contexts/appcontext'
import VesselMarker from './vesselMarker'
import React from 'react'

interface IVesselProps {
vessel: ISimpleVessel
isMonitored: boolean
}

export default function Vessel({ vessel, isMonitored }: IVesselProps) {
const [history, setHistory] = useState<ILocation[] | undefined>(undefined)
const [vesselDetail, setVesselDetail] = useState<IDetailedVessel | undefined>(undefined)
const { clientHandler } = useAppContext()
const Vessel = React.memo(
({ vessel }: IVesselProps) => {
const [history, setHistory] = useState<ILocation[] | undefined>(undefined)

return (
<>
<VesselMarker vessel={vessel} popup={vesselDetail ? <Popup vessel={vesselDetail} /> : <></>}></VesselMarker>
</>
)
}
return (
<>
<VesselMarker vessel={vessel} />
</>
)
},
(prevProps, nextProps) => {
return (
prevProps.vessel.location.point.lat === nextProps.vessel.location.point.lat &&
prevProps.vessel.location.point.lon === nextProps.vessel.location.point.lon &&
prevProps.vessel.location.heading === nextProps.vessel.location.heading
)
}
)

export default Vessel
40 changes: 40 additions & 0 deletions src/components/vesselMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import L from 'leaflet'
import { useState, useEffect } from 'react'
import { useMapEvents } from 'react-leaflet'
import { ISimpleVessel } from '../models/simpleVessel'
import Vessel from './vessel'

interface IVesselMapProps {
vessels: ISimpleVessel[]
}

export default function VesselMap({ vessels }: IVesselMapProps) {
const [mapBounds, setMapBounds] = useState<L.LatLngBounds | null>(null)

const map = useMapEvents({
moveend() {
setMapBounds(map.getBounds())
},
zoomend() {
setMapBounds(map.getBounds())
},
})

useEffect(() => {
setMapBounds(map.getBounds())
}, [map])

const visibleVessels = vessels.filter((vessel) => {
if (!mapBounds) return true
const vesselLatLng = new L.LatLng(vessel.location.point.lat, vessel.location.point.lon)
return mapBounds.contains(vesselLatLng)
})

return (
<>
{visibleVessels.map((vessel) => (
<Vessel vessel={vessel} key={vessel.mmsi}></Vessel>
))}
</>
)
}
53 changes: 23 additions & 30 deletions src/components/vesselMarker.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,44 @@
import { ISimpleVessel } from '../models/simpleVessel'
import L from 'leaflet'
import { Marker, Popup as LPopup } from 'react-leaflet'
import IVesselDetail from '../models/detailedVessel'
import React, { useState } from 'react'
import L from 'leaflet'
import { ISimpleVessel } from '../models/simpleVessel'
import ReactDOMServer from 'react-dom/server'
import { useVesselGuiContext } from '../contexts/vesselGuiContext'

import Popup from './popup'
import VesselSVG from '../svgs/vesselSVG'
import CircleSVG from '../svgs/circleSVG'
interface IVesselMarker {
vessel: ISimpleVessel
popup: React.ReactNode
}

export default function VesselMarker({ vessel, popup }: IVesselMarker) {
const [vesselDetails, setVesselDetails] = useState<IVesselDetail | undefined>(undefined)
export default function VesselMarker({ vessel }: IVesselMarker) {
const { selectedVesselmmsi, setSelectedVesselmmsi } = useVesselGuiContext()
const [markerRef, setMarkerRef] = useState<L.Marker | null>(null)

const icon = L.divIcon({
className: 'custom-div-icon',
html: vessel.location.heading
? `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-rocket" viewBox="0 0 16 16" style="${
selectedVesselmmsi === vessel?.mmsi ? 'color: #da3122;' : ''
} transform: translate(-10px, -10px) rotate(${vessel.location.heading}deg);">
<path d="M8 8c.828 0 1.5-.895 1.5-2S8.828 4 8 4s-1.5.895-1.5 2S7.172 8 8 8"/>
<path d="M11.953 8.81c-.195-3.388-.968-5.507-1.777-6.819C9.707 1.233 9.23.751 8.857.454a3.5 3.5 0 0 0-.463-.315A2 2 0 0 0 8.25.064.55.55 0 0 0 8 0a.55.55 0 0 0-.266.073 2 2 0 0 0-.142.08 4 4 0 0 0-.459.33c-.37.308-.844.803-1.31 1.57-.805 1.322-1.577 3.433-1.774 6.756l-1.497 1.826-.004.005A2.5 2.5 0 0 0 2 12.202V15.5a.5.5 0 0 0 .9.3l1.125-1.5c.166-.222.42-.4.752-.57.214-.108.414-.192.625-.281l.198-.084c.7.428 1.55.635 2.4.635s1.7-.207 2.4-.635q.1.044.196.083c.213.09.413.174.627.282.332.17.586.348.752.57l1.125 1.5a.5.5 0 0 0 .9-.3v-3.298a2.5 2.5 0 0 0-.548-1.562zM12 10.445v.055c0 .866-.284 1.585-.75 2.14.146.064.292.13.425.199.39.197.8.46 1.1.86L13 14v-1.798a1.5 1.5 0 0 0-.327-.935zM4.75 12.64C4.284 12.085 4 11.366 4 10.5v-.054l-.673.82a1.5 1.5 0 0 0-.327.936V14l.225-.3c.3-.4.71-.664 1.1-.861.133-.068.279-.135.425-.199M8.009 1.073q.096.06.226.163c.284.226.683.621 1.09 1.28C10.137 3.836 11 6.237 11 10.5c0 .858-.374 1.48-.943 1.893C9.517 12.786 8.781 13 8 13s-1.517-.214-2.057-.607C5.373 11.979 5 11.358 5 10.5c0-4.182.86-6.586 1.677-7.928.409-.67.81-1.082 1.096-1.32q.136-.113.236-.18Z"/>
<path d="M9.479 14.361c-.48.093-.98.139-1.479.139s-.999-.046-1.479-.139L7.6 15.8a.5.5 0 0 0 .8 0z"/>
</svg>`
: `
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" fill="currentColor" class="bi bi-circle-fill" viewBox="0 0 16 16"style="${
selectedVesselmmsi === vessel?.mmsi ? 'color: #da3122;' : ''
} transform: translate(-10px, -10px);">
<circle cx="8" cy="8" r="8"/>
</svg>
`,
? `${ReactDOMServer.renderToString(
<VesselSVG heading={vessel.location.heading} selected={selectedVesselmmsi === vessel.mmsi} />
)}`
: `${ReactDOMServer.renderToString(<CircleSVG selected={selectedVesselmmsi === vessel.mmsi} />)}`,
iconAnchor: [0, 0],
popupAnchor: [0, -25],
popupAnchor: [0, -15],
})

if (markerRef) {
markerRef.on('click', function (e) {
const handleVesselClick = () => {
if (selectedVesselmmsi === vessel.mmsi) {
setSelectedVesselmmsi(undefined)
} else {
setSelectedVesselmmsi(vessel.mmsi)
})
}
}

return (
<Marker position={[vessel.location.point.lat, vessel.location.point.lon]} icon={icon} ref={setMarkerRef}>
{vesselDetails && <LPopup>{popup}</LPopup>}
<Marker
eventHandlers={{ click: handleVesselClick }}
position={[vessel.location.point.lat, vessel.location.point.lon]}
icon={icon}
>
<LPopup>{selectedVesselmmsi === vessel.mmsi && <Popup mmsi={vessel.mmsi} />}</LPopup>
</Marker>
)
}
4 changes: 2 additions & 2 deletions src/contexts/appcontext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export const useAppContext = () => {
export default AppContext

export const AppContextProvider = ({ children }: { children: React.ReactNode }) => {
const [myDateTime, setMyDateTime] = useState<Date>(new Date())
const [myClockSpeed, setMyClockSpeed] = useState<number>(12)
const [myDateTime, setMyDateTime] = useState<Date>(new Date(1725844950)) ///this should be "new Date()" in the future
const [myClockSpeed, setMyClockSpeed] = useState<number>(1)

const grpcWeb = new GrpcWebImpl('http://localhost:8080', {})
const client = new AISServiceClientImpl(grpcWeb)
Expand Down
Loading

0 comments on commit e50cd85

Please sign in to comment.