Skip to content

Commit

Permalink
Merge pull request #122 from kieler/tracker-components
Browse files Browse the repository at this point in the history
Tracker status display
  • Loading branch information
n1kPLV authored Sep 8, 2023
2 parents 50f1cd6 + adeb32b commit fc4dad3
Show file tree
Hide file tree
Showing 13 changed files with 182 additions and 73 deletions.
2 changes: 2 additions & 0 deletions Server/src/models/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

/**
Expand Down Expand Up @@ -129,6 +130,7 @@ export type VehicleType = UpdateVehicleType & {
export type Tracker = {
id: string
vehicleId: number | null
battery?: number // ideally between 0 (empty) and 1 (full). But probably some arbitrary value...
data?: unknown
}

Expand Down
4 changes: 3 additions & 1 deletion Server/src/routes/track.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ export class TrackRoute {
? (await TrackService.getTrackKmAsPercentage(trackKm, track)) ?? undefined
: undefined
const heading = await VehicleService.getVehicleHeading(vehicle)
const speed = await VehicleService.getVehicleSpeed(vehicle)
return {
id: vehicle.uid,
track: vehicle.trackId,
Expand All @@ -239,7 +240,8 @@ export class TrackRoute {
trackerIds: (await database.trackers.getByVehicleId(vehicle.uid)).map(y => y.uid),
pos,
percentagePosition,
heading
heading,
speed
}
})
)
Expand Down
6 changes: 5 additions & 1 deletion Server/src/routes/tracker.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down
54 changes: 42 additions & 12 deletions Website/src/app/components/dynlist.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +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" });
Expand All @@ -16,14 +18,32 @@ const fetcher = async ([url, track_id]: [url: string, track_id: number]) => {
return res_2;
};

function FocusVehicleLink(props: { v: Vehicle }) {
return <Link href={`/map?focus=${props.v.id}`}>Link</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<boolean>;
FocusVehicle?: FunctionComponent<{ v: Vehicle }>;
}) {
const { data, error, isLoading } = useSWR(
logged_in && track_id ? ["/webapi/vehicles/list", track_id] : null,
fetcher,
Expand Down Expand Up @@ -54,11 +74,17 @@ export default function DynamicList({ server_vehicles, track_id, logged_in, trac
<thead>
<tr className={"my-2"}>
<th className={"mx-2 border-b-black dark:border-b-white border-b px-2"}>Name</th>
<th className={"mx-2 border-b-black dark:border-b-white hidden sm:table-cell border-b px-2"}>geog. Breite</th>
<th className={"mx-2 border-b-black dark:border-b-white hidden sm:table-cell border-b px-2"}>geog. Länge</th>
<th className={"mx-2 border-b-black dark:border-b-white hidden sm:table-cell border-b px-2"}>Richtung</th>
<th className={"mx-2 border-b-black dark:border-b-white hidden sm:table-cell border-b px-2"}>
geog. Breite
</th>
<th className={"mx-2 border-b-black dark:border-b-white hidden sm:table-cell border-b px-2"}>
geog. Länge
</th>
<th className={"mx-2 border-b-black dark:border-b-white hidden sm:table-cell border-b px-2"}>
Richtung
</th>
<th className={"mx-2 border-b-black dark:border-b-white border-b px-2"}>Batterieladung</th>
<th className={"mx-2 border-b-black dark:border-b-white border-b px-2"}>Auf Karte anzeigen</th>
<th className={"mx-2 border-b-black dark:border-b-white border-b px-2"}>Auf Karte zeigen</th>
</tr>
</thead>
<tbody>
Expand All @@ -74,11 +100,15 @@ export default function DynamicList({ server_vehicles, track_id, logged_in, trac
<td className={"mx-2 px-2 hidden sm:table-cell text-center"}>
{v.heading ? coordinateFormatter.format(v.heading) : "unbekannt"}
</td>
<td className={"mx-2 px-2 text-center"}>{{}.toString()}</td>
<td className={"mx-2 px-2 text-center"}>
<Link className="text-blue-600 visited:text-purple-700" href={`/map?focus=${v.id}`}>
Link
</Link>
<td className={"px-2 text-center"}>
<div className={"max-w-[16rem] mx-auto"}>
{v.trackerIds.map(trackerId => (
<TrackerCharge key={trackerId} trackerId={trackerId} />
))}
</div>
</td>
<td className={"px-2 text-center"}>
<FocusVehicle v={v} />
</td>
</tr>
))}
Expand Down
2 changes: 1 addition & 1 deletion Website/src/app/components/dynmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<div className={"h-96 grow"}>
<div className={"basis-96 grow relative"}>
<_internal_DynamicMap
position={position}
zoom_level={zoom_level}
Expand Down
21 changes: 10 additions & 11 deletions Website/src/app/components/login_wrap.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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<T extends object>({
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<T & { logged_in: boolean; setLogin: Dispatch<boolean> }>;
}) {
const [loginState, setLogin] = useState(logged_in);

// console.log('track selected', track_selected, map_conf.track_id)
Expand All @@ -39,9 +38,9 @@ const LoginWrapper = ({
</SelectionDialog>
)
)}
{child({ ...map_conf, logged_in: loginState, setLogin: setLogin })}
<Child {...childConf} logged_in={loginState} setLogin={setLogin} />
</>
);
};
}

export default LoginWrapper;
29 changes: 21 additions & 8 deletions Website/src/app/components/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ 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";
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");
Expand Down Expand Up @@ -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", () => {
Expand Down Expand Up @@ -224,7 +225,7 @@ function Map({

return (
<>
<div id="map" className="h-full" ref={mapContainerRef} />
<div id="map" className="absolute inset-0" ref={mapContainerRef} />
{/* If a vehicle is in focus, and we have a popup open, populate its contents with a portal from here. */}
{popupContainer &&
createPortal(
Expand All @@ -233,10 +234,16 @@ function Map({
<h2 className={"col-span-2 basis-full text-xl text-center"}>
Vehicle &quot;{vehicleInFocus?.name}&quot;
</h2>
<div className={"basis-1/2"}>Tracker-Level:</div>
<div className={"basis-1/2"}>{vehicleInFocus ? "TODO" : "unbekannt"}</div>
<div className={"basis-1/2"}>Position:</div>
<div className={"basis-1/2"}>
<div className={"basis-1/3"}>Tracker-Ladezustand:</div>
<div className={"basis-2/3"}>
{vehicleInFocus
? vehicleInFocus.trackerIds.map(trackerId => (
<TrackerCharge key={trackerId} trackerId={trackerId} />
))
: "unbekannt"}
</div>
<div className={"basis-1/3"}>Position:</div>
<div className={"basis-2/3"}>
{vehicleInFocus?.pos ? (
<>
{coordinateFormatter.format(vehicleInFocus?.pos.lat)} N{" "}
Expand All @@ -246,6 +253,12 @@ function Map({
"unbekannt"
)}
</div>
<div className={"basis-1/3"}>Geschwindigkeit:</div>
<div className={"basis-2/3"}>
{vehicleInFocus?.speed != undefined && vehicleInFocus.speed !== -1
? speedFormatter.format(vehicleInFocus.speed)
: "unbekannt"}
</div>
</>
) : (
<div />
Expand Down
29 changes: 29 additions & 0 deletions Website/src/app/components/tracker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { getFetcher, TrackerIdRoute } from "@/utils/fetcher";
import useSWR from "swr";
import { batteryLevelFormatter } from "@/utils/helpers";

export default function TrackerCharge({ trackerId }: { trackerId: string }) {
const safeTrackerId = encodeURIComponent(trackerId);
const { data: tracker_data } = useSWR(`/webapi/tracker/read/${safeTrackerId}`, getFetcher<TrackerIdRoute>);

return (
<>
{tracker_data && (
<div className={"w-full flex flex-nowrap my-1 gap-1"}>
<div className={"group relative sm:grow shrink min-w-0 basis-32 text-left"}>
<div className={"truncate w-32 sm:w-full max-w-full min-w-0"}>{tracker_data.id}</div>
<div
className={
"opacity-0 group-hover:opacity-100 z-10 transition-opacity pointer-events-none absolute dark:bg-gray-900 dark:text-white bg-gray-100 rounded py-2 px-3 top-8 -left-3 w-max"
}>
{tracker_data.id}
</div>
</div>
<div className={"basis-10 text-right shrink-0"}>
{tracker_data.battery == undefined ? "?" : batteryLevelFormatter.format(tracker_data.battery)}
</div>
</div>
)}
</>
);
}
41 changes: 14 additions & 27 deletions Website/src/app/list/page.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +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, []];
});

console.log("server vehicles", server_vehicles);
const listConf = {
server_vehicles,
track_data,
track_id
};

return (
<main className="container mx-auto max-w-4xl grow">
<main className="mx-auto w-full max-w-4xl grow">
<div className={"bg-white dark:bg-slate-800 dark:text-white p-4 rounded"}>
<LoginWrapper
logged_in={token !== undefined}
track_selected={track_selected}
map_conf={{
position: { lat: 54.2333, lng: 10.6024 },
zoom_level: 11,
server_vehicles,
track_data,
track_id,
points_of_interest,
poi_types
}}
childConf={listConf}
child={DynamicList}
/>
</div>
Expand Down
3 changes: 1 addition & 2 deletions Website/src/app/map/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -49,7 +48,7 @@ export default async function MapPage({ searchParams }: { searchParams: { focus?
<LoginWrapper
logged_in={token !== undefined}
track_selected={track_selected}
map_conf={{
childConf={{
position: { lat: 54.2333, lng: 10.6024 },
zoom_level: 11.5,
server_vehicles,
Expand Down
Loading

0 comments on commit fc4dad3

Please sign in to comment.