From e304184dfddfc3724c8eff13da5a27019c4f433f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=A4ckelmann?= <6890706+n1kPLV@users.noreply.github.com> Date: Sat, 2 Sep 2023 17:16:10 +0200 Subject: [PATCH 1/6] Preparations for tracker status display. Also fixes #105 --- Website/src/app/components/dynmap.tsx | 2 +- Website/src/app/components/map.tsx | 15 ++++---- Website/src/app/components/tracker.tsx | 11 ++++++ .../webapi/tracker/read/[trackerID]/route.ts | 35 +++++++++++++++++++ 4 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 Website/src/app/components/tracker.tsx create mode 100644 Website/src/app/webapi/tracker/read/[trackerID]/route.ts diff --git a/Website/src/app/components/dynmap.tsx b/Website/src/app/components/dynmap.tsx index e2e17e9c..04050572 100644 --- a/Website/src/app/components/dynmap.tsx +++ b/Website/src/app/components/dynmap.tsx @@ -68,7 +68,7 @@ export default function DynamicMap({ return ( // The map needs to have a specified height, so I chose 96 tailwind units. // The `grow` class will however still cause the map to take up the available space. -
+
<_internal_DynamicMap position={position} zoom_level={zoom_level} diff --git a/Website/src/app/components/map.tsx b/Website/src/app/components/map.tsx index d044ac66..198e258d 100644 --- a/Website/src/app/components/map.tsx +++ b/Website/src/app/components/map.tsx @@ -10,6 +10,7 @@ import { createPortal } from "react-dom"; import RotatingVehicleIcon from "@/utils/rotatingIcon"; import { PointOfInterest, POIType, POITypeIconValues } from "@/utils/api"; import { POIIconImg } from "@/utils/common"; +import TrackerCharge from "@/app/components/tracker"; function poiPopupFactory(poi: PointOfInterest, poi_type?: POIType): HTMLDivElement { const container = document.createElement("div"); @@ -167,8 +168,8 @@ function Map({ // create a div element to contain the popup content. // We can then use a React portal to place content in there. const popupElement = document.createElement("div"); - popupElement.className = "w-64 flex p-1.5 flex-row flex-wrap"; - m.bindPopup(popupElement); + popupElement.className = "w-96 flex p-1.5 flex-row flex-wrap"; + m.bindPopup(popupElement, {className: 'w-auto', maxWidth: undefined}); setPopupContainer(popupElement); // unset the focussed element on popup closing. m.on("popupclose", () => { @@ -224,7 +225,7 @@ function Map({ return ( <> -
+
{/* If a vehicle is in focus, and we have a popup open, populate its contents with a portal from here. */} {popupContainer && createPortal( @@ -233,10 +234,10 @@ function Map({

Vehicle "{vehicleInFocus?.name}"

-
Tracker-Level:
-
{vehicleInFocus ? "TODO" : "unbekannt"}
-
Position:
-
+
Tracker-Ladezustand:
+
{vehicleInFocus ? vehicleInFocus.trackerIds.map(trackerId => ) : "unbekannt"}
+
Position:
+
{vehicleInFocus?.pos ? ( <> {coordinateFormatter.format(vehicleInFocus?.pos.lat)} N{" "} diff --git a/Website/src/app/components/tracker.tsx b/Website/src/app/components/tracker.tsx new file mode 100644 index 00000000..8d35c5b5 --- /dev/null +++ b/Website/src/app/components/tracker.tsx @@ -0,0 +1,11 @@ +import {getFetcher, TrackerIdRoute} from "@/utils/fetcher"; +import useSWR from "swr"; + +export default function TrackerCharge({trackerId}: {trackerId: string}) { + + const safeTrackerId = encodeURIComponent(trackerId); + const {data: tracker_data} = useSWR(`/webapi/tracker/read/${safeTrackerId}`, getFetcher) + + return (
{tracker_data && <>{tracker_data.id}: {tracker_data.data ?? "unbekannt"} %}
) + +} \ No newline at end of file diff --git a/Website/src/app/webapi/tracker/read/[trackerID]/route.ts b/Website/src/app/webapi/tracker/read/[trackerID]/route.ts new file mode 100644 index 00000000..59d6d93a --- /dev/null +++ b/Website/src/app/webapi/tracker/read/[trackerID]/route.ts @@ -0,0 +1,35 @@ +import {apiError} from "@/utils/helpers"; +import {NextRequest, NextResponse} from "next/server"; +import {getTrackerById} from "@/utils/data"; +import {UnauthorizedError} from "@/utils/types"; + +export async function GET(request: NextRequest, {params: {trackerID}}: {params: {trackerID: string}}) { + + // check if the ID was valid. + if (!trackerID) { + console.log("Can not get tracker: ", trackerID, "is invalid"); + return apiError(404); + } + + // obtain the authentication token from the request cookies + const token = request.cookies.get("token")?.value; + + // If it is missing, the user is not logged in. + if (token == undefined) { + return apiError(401); + } + + try { + // initiate a deletion request on the backend + const res = await getTrackerById(token, trackerID); + + return NextResponse.json(res); + } catch (e) { + if (e instanceof UnauthorizedError) { + return apiError(401); + } + // If we throw an error, log it and declare a server error. + console.error("Cannot get tracker", trackerID, "- Reason: ", e); + return apiError(500); + } +} \ No newline at end of file From 866ffe5e0d6ec27fcd7cf8998f50e448f4e9b2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=A4ckelmann?= <6890706+n1kPLV@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:33:16 +0200 Subject: [PATCH 2/6] provide last known battery charge for each tracker --- Server/src/models/api.ts | 1 + Server/src/routes/tracker.route.ts | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Server/src/models/api.ts b/Server/src/models/api.ts index 9c9e1d14..0c63a109 100644 --- a/Server/src/models/api.ts +++ b/Server/src/models/api.ts @@ -129,6 +129,7 @@ export type VehicleType = UpdateVehicleType & { export type Tracker = { id: string vehicleId: number | null + battery?: number data?: unknown } diff --git a/Server/src/routes/tracker.route.ts b/Server/src/routes/tracker.route.ts index cd3225cc..c311eef8 100644 --- a/Server/src/routes/tracker.route.ts +++ b/Server/src/routes/tracker.route.ts @@ -4,7 +4,7 @@ import { authenticateJWT, jsonParser } from "." import TrackerService from "../services/tracker.service" import { UplinkTracker } from "../models/api.tracker" import please_dont_crash from "../utils/please_dont_crash" -import { Prisma, Tracker, Vehicle } from "@prisma/client" +import { Prisma, Tracker, Vehicle, Log } from "@prisma/client" import VehicleService from "../services/vehicle.service" import database from "../services/database.service" import { Tracker as APITracker } from "../models/api" @@ -70,9 +70,13 @@ export class TrackerRoute { return } + const [lastLog]: [lastLog?: Log, ...rest: never[]] = await database.logs.getAll(undefined, tracker.uid, 1) + + const apiTracker: APITracker = { id: tracker.uid, vehicleId: tracker.vehicleId, + battery: lastLog?.battery ?? undefined, data: tracker.data ?? undefined } From fa61be8e92b9bb35c3fe06f2f7bd7b0d3fe6a9bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=A4ckelmann?= <6890706+n1kPLV@users.noreply.github.com> Date: Mon, 4 Sep 2023 18:45:37 +0200 Subject: [PATCH 3/6] display tracker batery information (if known) --- Website/src/app/components/dynlist.tsx | 25 ++++++++++++++----- Website/src/app/components/tracker.tsx | 34 ++++++++++++++++++++------ Website/src/app/list/page.tsx | 3 +-- Website/src/utils/api.ts | 1 + Website/src/utils/helpers.ts | 18 +++++++------- 5 files changed, 56 insertions(+), 25 deletions(-) diff --git a/Website/src/app/components/dynlist.tsx b/Website/src/app/components/dynlist.tsx index 2e084af3..14713296 100644 --- a/Website/src/app/components/dynlist.tsx +++ b/Website/src/app/components/dynlist.tsx @@ -5,6 +5,7 @@ import { Vehicle } from "@/utils/api"; import useSWR from "swr"; import { coordinateFormatter } from "@/utils/helpers"; import Link from "next/link"; +import TrackerCharge from "@/app/components/tracker"; const fetcher = async ([url, track_id]: [url: string, track_id: number]) => { const res = await fetch(`${url}/${track_id}`, { method: "get" }); @@ -54,11 +55,17 @@ export default function DynamicList({ server_vehicles, track_id, logged_in, trac Name - geog. Breite - geog. Länge - Richtung + + geog. Breite + + + geog. Länge + + + Richtung + Batterieladung - Auf Karte anzeigen + Auf Karte zeigen @@ -74,8 +81,14 @@ export default function DynamicList({ server_vehicles, track_id, logged_in, trac {v.heading ? coordinateFormatter.format(v.heading) : "unbekannt"} - {{}.toString()} - + +
+ {v.trackerIds.map(trackerId => ( + + ))} +
+ + Link diff --git a/Website/src/app/components/tracker.tsx b/Website/src/app/components/tracker.tsx index 8d35c5b5..937e8d28 100644 --- a/Website/src/app/components/tracker.tsx +++ b/Website/src/app/components/tracker.tsx @@ -1,11 +1,29 @@ -import {getFetcher, TrackerIdRoute} from "@/utils/fetcher"; +import { getFetcher, TrackerIdRoute } from "@/utils/fetcher"; import useSWR from "swr"; +import { batteryLevelFormatter } from "@/utils/helpers"; -export default function TrackerCharge({trackerId}: {trackerId: string}) { +export default function TrackerCharge({ trackerId }: { trackerId: string }) { + const safeTrackerId = encodeURIComponent(trackerId); + const { data: tracker_data } = useSWR(`/webapi/tracker/read/${safeTrackerId}`, getFetcher); - const safeTrackerId = encodeURIComponent(trackerId); - const {data: tracker_data} = useSWR(`/webapi/tracker/read/${safeTrackerId}`, getFetcher) - - return (
{tracker_data && <>{tracker_data.id}: {tracker_data.data ?? "unbekannt"} %}
) - -} \ No newline at end of file + return ( + <> + {tracker_data && ( +
+
+
{tracker_data.id}
+
+ {tracker_data.id} +
+
+
+ {tracker_data.battery == undefined ? "?" : batteryLevelFormatter.format(tracker_data.battery)} +
+
+ )} + + ); +} diff --git a/Website/src/app/list/page.tsx b/Website/src/app/list/page.tsx index fb2d82c7..b0512c73 100644 --- a/Website/src/app/list/page.tsx +++ b/Website/src/app/list/page.tsx @@ -25,9 +25,8 @@ export default async function Home() { return [undefined, [], [], []]; }); - console.log("server vehicles", server_vehicles); return ( -
+
(f: () => T): T | undefined { try { return f(); } catch (e) { - return + return; } } export function apiError(statusCode: number): NextResponse { - const statusText = http.STATUS_CODES[statusCode] + const statusText = http.STATUS_CODES[statusCode]; return new NextResponse(statusText + "\r\n", { status: statusCode, @@ -60,5 +60,5 @@ export function getUsername(token: string): string { if (!isTokenPayload(payload)) { throw new TypeError("Not a valid jwt auth token"); } - return payload.username + return payload.username; } From 616a5fa28c38f18bcbe47c82c88e8a17c590d85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=A4ckelmann?= <6890706+n1kPLV@users.noreply.github.com> Date: Mon, 4 Sep 2023 22:53:25 +0200 Subject: [PATCH 4/6] Small dark mode improvements --- Website/src/app/components/tracker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Website/src/app/components/tracker.tsx b/Website/src/app/components/tracker.tsx index 937e8d28..5f343e10 100644 --- a/Website/src/app/components/tracker.tsx +++ b/Website/src/app/components/tracker.tsx @@ -14,7 +14,7 @@ export default function TrackerCharge({ trackerId }: { trackerId: string }) {
{tracker_data.id}
{tracker_data.id}
From 890b40783dddad5e1c0479c5f20557b73efa0273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=A4ckelmann?= <6890706+n1kPLV@users.noreply.github.com> Date: Wed, 6 Sep 2023 17:15:04 +0200 Subject: [PATCH 5/6] Also provide vehicle speed on request --- Server/src/models/api.ts | 3 ++- Server/src/routes/track.route.ts | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Server/src/models/api.ts b/Server/src/models/api.ts index 0c63a109..c4ea3ba0 100644 --- a/Server/src/models/api.ts +++ b/Server/src/models/api.ts @@ -102,6 +102,7 @@ export type Vehicle = UpdateVehicle & { pos?: Position // undefined if position is unknown. percentagePosition?: number // A position mapped onto percentage 0-100) e.g. 0% Malente; 100% Lütjenburg heading?: number // between 0 and 360 + speed?: number // in km/h } /** @@ -129,7 +130,7 @@ export type VehicleType = UpdateVehicleType & { export type Tracker = { id: string vehicleId: number | null - battery?: number + battery?: number // ideally between 0 (empty) and 1 (full). But probably some arbitrary value... data?: unknown } diff --git a/Server/src/routes/track.route.ts b/Server/src/routes/track.route.ts index d670083e..c638b0f2 100644 --- a/Server/src/routes/track.route.ts +++ b/Server/src/routes/track.route.ts @@ -228,6 +228,7 @@ export class TrackRoute { // This might not make much sense. const percentagePosition = (await VehicleService.getVehicleTrackDistancePercentage(vehicle)) ?? undefined const heading = await VehicleService.getVehicleHeading(vehicle) + const speed = await VehicleService.getVehicleSpeed(vehicle) return { id: vehicle.uid, track: vehicle.trackId, @@ -236,7 +237,8 @@ export class TrackRoute { trackerIds: (await database.trackers.getByVehicleId(vehicle.uid)).map(y => y.uid), pos, percentagePosition, - heading + heading, + speed } }) ) From adeb32bee31c674f7a9c03886ec62f011a190f81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=A4ckelmann?= <6890706+n1kPLV@users.noreply.github.com> Date: Wed, 6 Sep 2023 17:18:55 +0200 Subject: [PATCH 6/6] Add display of vehicle speed --- Website/src/app/components/dynlist.tsx | 29 +++++++++++++---- Website/src/app/components/login_wrap.tsx | 21 ++++++------- Website/src/app/components/map.tsx | 18 +++++++++-- Website/src/app/list/page.tsx | 38 ++++++++--------------- Website/src/app/map/page.tsx | 3 +- Website/src/utils/api.ts | 1 + Website/src/utils/helpers.ts | 9 +++++- 7 files changed, 71 insertions(+), 48 deletions(-) diff --git a/Website/src/app/components/dynlist.tsx b/Website/src/app/components/dynlist.tsx index 14713296..f1fd38fc 100644 --- a/Website/src/app/components/dynlist.tsx +++ b/Website/src/app/components/dynlist.tsx @@ -1,11 +1,12 @@ "use client"; -import { IMapRefreshConfig, RevalidateError } from "@/utils/types"; -import { Vehicle } from "@/utils/api"; +import { RevalidateError } from "@/utils/types"; +import { FullTrack, Vehicle } from "@/utils/api"; import useSWR from "swr"; import { coordinateFormatter } from "@/utils/helpers"; import Link from "next/link"; import TrackerCharge from "@/app/components/tracker"; +import { Dispatch, FunctionComponent } from "react"; const fetcher = async ([url, track_id]: [url: string, track_id: number]) => { const res = await fetch(`${url}/${track_id}`, { method: "get" }); @@ -17,14 +18,32 @@ const fetcher = async ([url, track_id]: [url: string, track_id: number]) => { return res_2; }; +function FocusVehicleLink(props: { v: Vehicle }) { + return Link; +} + /** * A dynamic list of vehicles with their current position. * @param server_vehicles A pre-fetched list of vehicles to be used until this data is fetched on the client side. * @param track_id The id of the currently selected track. # TODO: remove redundant variable -> already in track_data * @param logged_in A boolean indicating if the user is logged in. * @param track_data The information about the currently selected track. + * @param FocusVehicle Component to focus the specific vehicle. */ -export default function DynamicList({ server_vehicles, track_id, logged_in, track_data }: IMapRefreshConfig) { +export default function DynamicList({ + server_vehicles, + track_id, + logged_in, + track_data, + FocusVehicle = FocusVehicleLink +}: { + server_vehicles: Vehicle[]; + track_id: number; + logged_in: boolean; + track_data?: FullTrack; + setLogin: Dispatch; + FocusVehicle?: FunctionComponent<{ v: Vehicle }>; +}) { const { data, error, isLoading } = useSWR( logged_in && track_id ? ["/webapi/vehicles/list", track_id] : null, fetcher, @@ -89,9 +108,7 @@ export default function DynamicList({ server_vehicles, track_id, logged_in, trac
- - Link - + ))} diff --git a/Website/src/app/components/login_wrap.tsx b/Website/src/app/components/login_wrap.tsx index 0ee1fa64..318ef70e 100644 --- a/Website/src/app/components/login_wrap.tsx +++ b/Website/src/app/components/login_wrap.tsx @@ -1,6 +1,5 @@ "use client"; -import { IMapRefreshConfig } from "@/utils/types"; -import { useState } from "react"; +import { Dispatch, FunctionComponent, useState } from "react"; import { LoginDialog } from "@/app/components/login"; import { SelectionDialog } from "@/app/components/track_selection"; @@ -9,19 +8,19 @@ import { SelectionDialog } from "@/app/components/track_selection"; * @param logged_in initial login state * @param track_selected track selection state * @param map_conf parameters for the construction of the child - * @param child Function contructing the wrapped React Component. + * @param Child The wrapped React Component. */ -const LoginWrapper = ({ +function LoginWrapper({ logged_in, track_selected, - map_conf, - child + childConf, + child: Child }: { logged_in: boolean; track_selected: boolean; - map_conf: IMapRefreshConfig; - child: (conf: IMapRefreshConfig) => JSX.Element; -}) => { + childConf: T; + child: FunctionComponent }>; +}) { const [loginState, setLogin] = useState(logged_in); // console.log('track selected', track_selected, map_conf.track_id) @@ -39,9 +38,9 @@ const LoginWrapper = ({ ) )} - {child({ ...map_conf, logged_in: loginState, setLogin: setLogin })} + ); -}; +} export default LoginWrapper; diff --git a/Website/src/app/components/map.tsx b/Website/src/app/components/map.tsx index 198e258d..09f892eb 100644 --- a/Website/src/app/components/map.tsx +++ b/Website/src/app/components/map.tsx @@ -4,7 +4,7 @@ import "leaflet-rotatedmarker"; import "leaflet/dist/leaflet.css"; import { useEffect, useMemo, useRef, useState } from "react"; import { IMapConfig } from "@/utils/types"; -import { coordinateFormatter } from "@/utils/helpers"; +import { coordinateFormatter, speedFormatter } from "@/utils/helpers"; import assert from "assert"; import { createPortal } from "react-dom"; import RotatingVehicleIcon from "@/utils/rotatingIcon"; @@ -169,7 +169,7 @@ function Map({ // We can then use a React portal to place content in there. const popupElement = document.createElement("div"); popupElement.className = "w-96 flex p-1.5 flex-row flex-wrap"; - m.bindPopup(popupElement, {className: 'w-auto', maxWidth: undefined}); + m.bindPopup(popupElement, { className: "w-auto", maxWidth: undefined }); setPopupContainer(popupElement); // unset the focussed element on popup closing. m.on("popupclose", () => { @@ -235,7 +235,13 @@ function Map({ Vehicle "{vehicleInFocus?.name}"
Tracker-Ladezustand:
-
{vehicleInFocus ? vehicleInFocus.trackerIds.map(trackerId => ) : "unbekannt"}
+
+ {vehicleInFocus + ? vehicleInFocus.trackerIds.map(trackerId => ( + + )) + : "unbekannt"} +
Position:
{vehicleInFocus?.pos ? ( @@ -247,6 +253,12 @@ function Map({ "unbekannt" )}
+
Geschwindigkeit:
+
+ {vehicleInFocus?.speed != undefined && vehicleInFocus.speed !== -1 + ? speedFormatter.format(vehicleInFocus.speed) + : "unbekannt"} +
) : (
diff --git a/Website/src/app/list/page.tsx b/Website/src/app/list/page.tsx index b0512c73..f17b5b38 100644 --- a/Website/src/app/list/page.tsx +++ b/Website/src/app/list/page.tsx @@ -1,45 +1,33 @@ import { cookies } from "next/headers"; -import { getTrackData, getAllVehiclesOnTrack, getAllPOIsOnTrack, getAllPOITypes } from "@/utils/data"; +import { getAllVehiclesOnTrack, getTrackData } from "@/utils/data"; import LoginWrapper from "@/app/components/login_wrap"; -import { FullTrack, PointOfInterest, POIType, Vehicle } from "@/utils/api"; +import { FullTrack, Vehicle } from "@/utils/api"; import DynamicList from "@/app/components/dynlist"; export default async function Home() { const token = cookies().get("token")?.value; const track_id = parseInt(cookies().get("track_id")?.value ?? "", 10); const track_selected = !isNaN(track_id); - const [track_data, server_vehicles, points_of_interest, poi_types]: [ - FullTrack | undefined, - Vehicle[], - PointOfInterest[], - POIType[] - ] = !(token && track_selected) - ? [undefined, [] as Vehicle[], [] as PointOfInterest[], [] as POIType[]] - : await Promise.all([ - getTrackData(token, track_id), - getAllVehiclesOnTrack(token, track_id), - getAllPOIsOnTrack(token, track_id), - getAllPOITypes(token) - ]).catch(e => { + const [track_data, server_vehicles]: [FullTrack | undefined, Vehicle[]] = !(token && track_selected) + ? [undefined, [] as Vehicle[]] + : await Promise.all([getTrackData(token, track_id), getAllVehiclesOnTrack(token, track_id)]).catch(e => { console.error("Error fetching Map Data from the Backend:", e); - return [undefined, [], [], []]; + return [undefined, []]; }); + const listConf = { + server_vehicles, + track_data, + track_id + }; + return (
diff --git a/Website/src/app/map/page.tsx b/Website/src/app/map/page.tsx index 18ae3022..baf877f6 100644 --- a/Website/src/app/map/page.tsx +++ b/Website/src/app/map/page.tsx @@ -8,7 +8,6 @@ import { FullTrack, PointOfInterest, POIType, Vehicle } from "@/utils/api"; import { nanToUndefined } from "@/utils/helpers"; export default async function MapPage({ searchParams }: { searchParams: { focus?: string; success?: string } }) { - // get the login token and the ID of the selected track const token = cookies().get("token")?.value; const track_id = parseInt(cookies().get("track_id")?.value ?? "", 10); @@ -49,7 +48,7 @@ export default async function MapPage({ searchParams }: { searchParams: { focus? Promise = time => export const batteryLevelFormatter = new Intl.NumberFormat("de-DE", { notation: "standard", - style: "unit", + style: "percent", unit: "percent", maximumFractionDigits: 1 }); @@ -20,6 +20,13 @@ export const coordinateFormatter = new Intl.NumberFormat("de-DE", { maximumFractionDigits: 4 }); +export const speedFormatter = new Intl.NumberFormat("de-DE", { + notation: "standard", + style: "unit", + unit: "kilometer-per-hour", + maximumFractionDigits: 1 +}); + export function nanToUndefined(x: number): number | undefined { if (Number.isNaN(x)) return; return x;