diff --git a/Server/src/models/api.ts b/Server/src/models/api.ts index 28f009bb..9c9e1d14 100644 --- a/Server/src/models/api.ts +++ b/Server/src/models/api.ts @@ -37,10 +37,20 @@ export type FullTrack = BareTrack & { /** @see {isTrackList} ts-auto-guard:type-guard */ export type TrackList = BareTrack[] +/** @see {isCreatePOITypeIcon} ts-auto-guard:type-guard */ +export enum POITypeIcon { + Generic = 0, + LevelCrossing = 1, + LesserLevelCrossing = 2, + Picnic = 3, + TrackEnd = 4, + TurningPoint = 5, +} + /** @see {isCreatePOIType} ts-auto-guard:type-guard */ export type CreatePOIType = { name: string - icon: string + icon: POITypeIcon description?: string } diff --git a/Server/src/routes/init.route.ts b/Server/src/routes/init.route.ts index 008fb8e6..e39d6e95 100644 --- a/Server/src/routes/init.route.ts +++ b/Server/src/routes/init.route.ts @@ -1,10 +1,10 @@ import { Request, Response, Router } from "express" import { jsonParser } from "." import { InitRequestApp, InitResponseApp, TrackListEntryApp } from "../models/api.app" -import { PointOfInterest, Position } from "../models/api" +import { PointOfInterest, POITypeIcon, Position } from "../models/api" import { logger } from "../utils/logger" import TrackService from "../services/track.service" -import { POI, Track } from "@prisma/client" +import { POI, POIType, Track } from "@prisma/client" import POIService from "../services/poi.service" import { Feature, FeatureCollection, GeoJsonProperties, LineString, Point } from "geojson" import GeoJSONUtils from "../utils/geojsonUtils" @@ -195,11 +195,22 @@ export class InitRoute { private async getAppPoisFromDbPoi(pois: POI[]): Promise { const apiPois: PointOfInterest[] = [] for (const poi of pois) { - const type: number = poi.typeId + const type: POIType | null = await database.pois.getTypeById(poi.typeId) if (!type) { logger.error(`Could not determine type of poi with id ${poi.uid}`) continue } + const poiIcon: number = Number.parseInt(type.icon) + if (!Number.isInteger(poiIcon)) { + logger.error(`Icon of type with id ${type.uid} is not an integer.`) + continue + } + // Check if the icon number is a member of the enum. + if (!(poiIcon in POITypeIcon)) { + logger.warn(`Icon of type with id ${type.uid} is ${poiIcon}, not one of the known icons.`) + } + // ensure that the app always gets an enum member. + const appType: POITypeIcon = poiIcon in POITypeIcon ? poiIcon : POITypeIcon.Generic const geoJsonPos: Feature | null = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) if (!geoJsonPos) { @@ -219,7 +230,7 @@ export class InitRoute { apiPois.push({ id: poi.uid, name: poi.name, - typeId: 0 <= type && type <= 4 ? type : 0, + typeId: appType, pos: pos, percentagePosition: percentagePosition, isTurningPoint: poi.isTurningPoint, diff --git a/Server/src/routes/poi.route.ts b/Server/src/routes/poi.route.ts index 88b72044..73b8cff8 100644 --- a/Server/src/routes/poi.route.ts +++ b/Server/src/routes/poi.route.ts @@ -142,7 +142,7 @@ export class PoiRoute { properties: null } - const type: POIType | null = await POIService.getPOITypeById(userData.typeId) + const type: POIType | null = await database.pois.getTypeById(userData.typeId) if (!type) { logger.error(`Could not find poi type with id ${userData.typeId}`) res.sendStatus(400) @@ -179,7 +179,7 @@ export class PoiRoute { return } - const poiToUpdate: POI | null = await POIService.getPOIById(poiId) + const poiToUpdate: POI | null = await database.pois.getById(poiId) if (!poiToUpdate) { logger.error(`Could not find poi with id ${userData.id}`) res.sendStatus(404) @@ -234,13 +234,13 @@ export class PoiRoute { } // Look out for the POI - const poi: POI | null = await POIService.getPOIById(poiId) + const poi: POI | null = await database.pois.getById(poiId) if (!poi) { logger.error(`Could not find poi with id ${poiId}`) res.sendStatus(500) return } - const success: boolean = await POIService.removePOI(poi) + const success: boolean = await database.pois.remove(poi.uid) if (!success) { logger.error(`Could not delete poi with id ${poiId}`) res.sendStatus(500) diff --git a/Server/src/routes/poitype.route.ts b/Server/src/routes/poitype.route.ts index 7f6182eb..d3e015aa 100644 --- a/Server/src/routes/poitype.route.ts +++ b/Server/src/routes/poitype.route.ts @@ -45,7 +45,7 @@ export class PoiTypeRoute { const type: APIPoiType = { id: uid, name, - icon, + icon: Number.parseInt(icon), description: description ?? undefined } return type @@ -74,7 +74,7 @@ export class PoiTypeRoute { id: poiType.uid, name: poiType.name, description: poiType.description ?? undefined, - icon: poiType.icon + icon: Number.parseInt(poiType.icon) } res.json(apiPoiType) @@ -82,9 +82,10 @@ export class PoiTypeRoute { } private async createType(req: Request, res: Response): Promise { + // TODO: ensure that the icon is a member of the enum (or check if the type guard checks that) const { name, icon, description }: CreatePOIType = req.body - const poiType: POIType | null = await database.pois.saveType({ name, icon, description }) + const poiType: POIType | null = await database.pois.saveType({ name, icon: icon.toString(), description }) if (!poiType) { logger.error("Could not create poi type") res.sendStatus(500) @@ -94,7 +95,7 @@ export class PoiTypeRoute { const responseType: APIPoiType = { id: poiType.uid, name: poiType.name, - icon: poiType.icon, + icon: Number.parseInt(poiType.icon), description: poiType.description ?? undefined } res.status(201).json(responseType) @@ -104,6 +105,7 @@ export class PoiTypeRoute { private async updateType(req: Request, res: Response): Promise { const typeId: number = parseInt(req.params.typeId) const userData: APIPoiType = req.body + // TODO: ensure that the icon is a member of the enum (or check if the type guard checks that) if (userData.id !== typeId) { res.sendStatus(400) return @@ -118,7 +120,7 @@ export class PoiTypeRoute { type = await database.pois.updateType(typeId, { name: userData.name, - icon: userData.icon, + icon: userData.icon.toString(), description: userData.description }) if (!type) { diff --git a/Server/src/routes/track.route.ts b/Server/src/routes/track.route.ts index e1d6daf1..d670083e 100644 --- a/Server/src/routes/track.route.ts +++ b/Server/src/routes/track.route.ts @@ -4,13 +4,12 @@ import TrackService from "../services/track.service" import { POI, Track, Vehicle } from "@prisma/client" import please_dont_crash from "../utils/please_dont_crash" import { logger } from "../utils/logger" -import { UpdateTrack, BareTrack, FullTrack, PointOfInterest, Position, Vehicle as APIVehicle } from "../models/api" +import { BareTrack, FullTrack, PointOfInterest, Position, UpdateTrack, Vehicle as APIVehicle } from "../models/api" import VehicleService from "../services/vehicle.service" import { Feature, GeoJsonProperties, LineString, Point } from "geojson" import POIService from "../services/poi.service" import GeoJSONUtils from "../utils/geojsonUtils" import database from "../services/database.service" -import TrackerService from "../services/tracker.service" /** * The router class for the routing of the track uploads from the website. @@ -234,7 +233,7 @@ export class TrackRoute { track: vehicle.trackId, name: vehicle.name ? vehicle.name : "Empty Name", type: vehicle.typeId, - trackerIds: (await TrackerService.getTrackerByVehicle(vehicle.uid)).map(y => y.uid), + trackerIds: (await database.trackers.getByVehicleId(vehicle.uid)).map(y => y.uid), pos, percentagePosition, heading diff --git a/Server/src/routes/tracker.route.ts b/Server/src/routes/tracker.route.ts index 87c83291..cd3225cc 100644 --- a/Server/src/routes/tracker.route.ts +++ b/Server/src/routes/tracker.route.ts @@ -159,7 +159,7 @@ export class TrackerRoute { } const trackerId = trackerData.end_device_ids.device_id // load the tracker from the database - const tracker: Tracker | null = await TrackerService.getTrackerById(trackerId) + const tracker: Tracker | null = await database.trackers.getById(trackerId) if (!tracker) { logger.silly(`Tried to append log on unknown tracker with id ${trackerId}`) res.sendStatus(401) @@ -172,7 +172,7 @@ export class TrackerRoute { } // and get the vehicle the tracker is attached to. If it has no associated vehicle, do nothing. const associatedVehicle: Vehicle | null = tracker.vehicleId - ? await VehicleService.getVehicleById(tracker.vehicleId) + ? await database.vehicles.getById(tracker.vehicleId) : null if (!associatedVehicle) { logger.silly(`Got position from tracker ${trackerId} with no associated vehicle.`) diff --git a/Server/src/routes/user.route.ts b/Server/src/routes/user.route.ts index c689a2b8..d4ded347 100644 --- a/Server/src/routes/user.route.ts +++ b/Server/src/routes/user.route.ts @@ -123,11 +123,11 @@ export class UserRoute { /** * Delete a user with a certain uid. - * @param req A request containing a userId in its parameters. + * @param _req A request containing a userId in its parameters. * @param res * @returns Nothing */ - private async deleteUser(req: Request, res: Response): Promise { + private async deleteUser(_req: Request, res: Response): Promise { if (!res.locals || !res.locals.username) { res.sendStatus(400) return diff --git a/Server/src/routes/vehicle.route.ts b/Server/src/routes/vehicle.route.ts index bf288b4f..c0fdfcd4 100644 --- a/Server/src/routes/vehicle.route.ts +++ b/Server/src/routes/vehicle.route.ts @@ -45,13 +45,173 @@ export class VehicleRoute { return VehicleRoute.instance.router } + /** + * Map the vehicle name to the uid of the backend. + * + * @param req A request containing a `GetUidApp` with a vehicle name in the request body and a track id in the parameters + * to determine the vehicle. + * @param res The vehicles uid in a `ReturnUidApp`. + * @returns Nothing + */ + private async getUid(req: Request, res: Response): Promise { + const userData: GetUidApp = req.body + if (!userData || !userData.trackId || !userData.vehicleName) { + res.sendStatus(400) + return + } + + const track: Track | null = await database.tracks.getById(userData.trackId) + if (!track) { + logger.error(`Could not find track with id ${userData.trackId}`) + res.sendStatus(404) + return + } + + const vehicle: Vehicle | null = await database.vehicles.getByName(userData.vehicleName, track.uid) + if (!vehicle) { + res.sendStatus(404) + return + } + + const ret: ReturnUidApp = { vehicleId: vehicle.uid } + res.json(ret) + return + } + + /** + * Updates location of app and gets some present information for the app. (vehicles near user etc.) + * @param req An UpdateRequestWithLocationEnabled in the body. + * @param res An UpdateResponseWithLocationEnabled with the useful information. + * @returns Nothing. + */ + private async updateVehicleApp(req: Request, res: Response): Promise { + const userData: UpdateRequestApp = req.body + if (!userData) { + res.sendStatus(400) + return + } + + const userVehicle: Vehicle | null = await database.vehicles.getById(userData.vehicleId) + if (!userVehicle) { + logger.error(`Could not find vehicle with id ${userData.vehicleId}`) + res.sendStatus(404) + return + } + + // TODO: validate before with zod, jsonschema, io-ts, ts-auto-guard + if (userData.pos && userData.heading && userData.speed) { + const log: Log | null = await VehicleService.appendLog( + userVehicle.uid, + userData.pos, + userData.heading, + userData.speed + ) + if (!log) { + logger.warn(`Could not append log for user vehicle with id ${userVehicle.uid}`) + } + } + + const pos: Feature | null = await VehicleService.getVehiclePosition(userVehicle) + if (!pos) { + logger.error(`Could not find position of vehicle with id ${userVehicle.uid}`) + res.sendStatus(404) + return + } + const position: Position = { lat: GeoJSONUtils.getLatitude(pos), lng: GeoJSONUtils.getLongitude(pos) } + const heading: number = await VehicleService.getVehicleHeading(userVehicle) + const track: Track | null = await database.tracks.getById(userVehicle.trackId) + if (!track) { + logger.error(`Could not find track with id ${userVehicle.trackId} + obtained from the user vehicle with id ${userVehicle.uid}`) + res.sendStatus(500) + return + } + const userVehicleTrackKm: number | null = await VehicleService.getVehicleTrackDistanceKm(userVehicle) + if (!userVehicleTrackKm) { + logger.error(`Could not compute track kilometer for vehicle with id ${userVehicle.uid} + at track wit id ${userVehicle.trackId}`) + res.sendStatus(500) + return + } + const userVehicleSimplifiedHeading: number = await VehicleService.getVehicleTrackHeading( + userVehicle, + userVehicleTrackKm + ) + + const nearbyVehicles: Vehicle[] | null = await VehicleService.getNearbyVehicles(pos) + if (nearbyVehicles == null) { + res.sendStatus(500) + return + } + + const appVehiclesNearUser: VehicleApp[] = ( + await Promise.all( + nearbyVehicles.map(async v => { + const pos = await VehicleService.getVehiclePosition(v) + const trackers = await database.trackers.getByVehicleId(v.uid) + const nearbyVehicleTrackKm: number | null = await VehicleService.getVehicleTrackDistanceKm(v) + if (!nearbyVehicleTrackKm) { + logger.error(`Could not compute track kilometer for vehicle with id ${v.uid} + at track wit id ${v.trackId}`) + return { + id: v.uid, + name: v.name, + track: v.trackId, + type: v.typeId, + trackerIds: trackers.map(t => t.uid), + pos: pos ? { lat: GeoJSONUtils.getLatitude(pos), lng: GeoJSONUtils.getLongitude(pos) } : undefined, + percentagePosition: (await VehicleService.getVehicleTrackDistancePercentage(v)) ?? -1, + heading: await VehicleService.getVehicleHeading(v), + headingTowardsUser: undefined + } + } + const nearbySimplifiedVehicleHeading: number = await VehicleService.getVehicleTrackHeading( + v, + nearbyVehicleTrackKm + ) + return { + id: v.uid, + name: v.name, + track: v.trackId, + type: v.typeId, + trackerIds: trackers.map(t => t.uid), + pos: pos ? { lat: GeoJSONUtils.getLatitude(pos), lng: GeoJSONUtils.getLongitude(pos) } : undefined, + percentagePosition: (await VehicleService.getVehicleTrackDistancePercentage(v)) ?? -1, + heading: await VehicleService.getVehicleHeading(v), + headingTowardsUser: + userVehicleSimplifiedHeading !== 0 && nearbySimplifiedVehicleHeading !== 0 + ? nearbySimplifiedVehicleHeading != userVehicleSimplifiedHeading + : undefined + } + }) + ) + ).filter(v => v.id !== userVehicle.uid && v.track === track.uid && v.percentagePosition !== -1) + + const percentagePositionOnTrack: number | null = await VehicleService.getVehicleTrackDistancePercentage(userVehicle) + if (!percentagePositionOnTrack) { + logger.error(`Could not determine percentage position on track for user with vehicle ${userVehicle.uid}`) + res.sendStatus(500) + return + } + const ret: UpdateResponseApp = { + pos: position, + heading: heading, + vehiclesNearUser: appVehiclesNearUser, + speed: await VehicleService.getVehicleSpeed(userVehicle), + percentagePositionOnTrack: percentagePositionOnTrack, + passingPosition: undefined // TODO: Find out passingPosition + } + res.json(ret) + return + } + /** * Get a list of vehicles with all the required properties for CRUD operations - * @param req A request containing a track id in the parameters + * @param _req A request containing a track id in the parameters * @param res A list of `VehicleListItemWebsite`. * @returns Nothing */ - private async getAllVehicles(req: Request, res: Response): Promise { + private async getAllVehicles(_req: Request, res: Response): Promise { const vehicles = await database.vehicles.getAll() const apiVehicles: APIVehicle[] = await Promise.all( vehicles.map(async vehicle => { @@ -281,164 +441,4 @@ export class VehicleRoute { res.sendStatus(200) return } - - /** - * Map the vehicle name to the uid of the backend. - * - * @param req A request containing a `GetUidApp` with a vehicle name in the request body and a track id in the parameters - * to determine the vehicle. - * @param res The vehicles uid in a `ReturnUidApp`. - * @returns Nothing - */ - private async getUid(req: Request, res: Response): Promise { - const userData: GetUidApp = req.body - if (!userData || !userData.trackId || !userData.vehicleName) { - res.sendStatus(400) - return - } - - const track: Track | null = await database.tracks.getById(userData.trackId) - if (!track) { - logger.error(`Could not find track with id ${userData.trackId}`) - res.sendStatus(404) - return - } - - const vehicle: Vehicle | null = await database.vehicles.getByName(userData.vehicleName, track.uid) - if (!vehicle) { - res.sendStatus(404) - return - } - - const ret: ReturnUidApp = { vehicleId: vehicle.uid } - res.json(ret) - return - } - - /** - * Updates location of app and gets some present information for the app. (vehicles near user etc.) - * @param req An UpdateRequestWithLocationEnabled in the body. - * @param res An UpdateResponseWithLocationEnabled with the useful information. - * @returns Nothing. - */ - private async updateVehicleApp(req: Request, res: Response): Promise { - const userData: UpdateRequestApp = req.body - if (!userData) { - res.sendStatus(400) - return - } - - const userVehicle: Vehicle | null = await VehicleService.getVehicleById(userData.vehicleId) - if (!userVehicle) { - logger.error(`Could not find vehicle with id ${userData.vehicleId}`) - res.sendStatus(404) - return - } - - // TODO: validate before with zod, jsonschema, io-ts, ts-auto-guard - if (userData.pos && userData.heading && userData.speed) { - const log: Log | null = await VehicleService.appendLog( - userVehicle.uid, - userData.pos, - userData.heading, - userData.speed - ) - if (!log) { - logger.warn(`Could not append log for user vehicle with id ${userVehicle.uid}`) - } - } - - const pos: Feature | null = await VehicleService.getVehiclePosition(userVehicle) - if (!pos) { - logger.error(`Could not find position of vehicle with id ${userVehicle.uid}`) - res.sendStatus(404) - return - } - const position: Position = { lat: GeoJSONUtils.getLatitude(pos), lng: GeoJSONUtils.getLongitude(pos) } - const heading: number = await VehicleService.getVehicleHeading(userVehicle) - const track: Track | null = await database.tracks.getById(userVehicle.trackId) - if (!track) { - logger.error(`Could not find track with id ${userVehicle.trackId} - obtained from the user vehicle with id ${userVehicle.uid}`) - res.sendStatus(500) - return - } - const userVehicleTrackKm: number | null = await VehicleService.getVehicleTrackDistanceKm(userVehicle) - if (!userVehicleTrackKm) { - logger.error(`Could not compute track kilometer for vehicle with id ${userVehicle.uid} - at track wit id ${userVehicle.trackId}`) - res.sendStatus(500) - return - } - const userVehicleSimplifiedHeading: number = await VehicleService.getVehicleTrackHeading( - userVehicle, - userVehicleTrackKm - ) - - const nearbyVehicles: Vehicle[] | null = await VehicleService.getNearbyVehicles(pos) - if (nearbyVehicles == null) { - res.sendStatus(500) - return - } - - const appVehiclesNearUser: VehicleApp[] = ( - await Promise.all( - nearbyVehicles.map(async v => { - const pos = await VehicleService.getVehiclePosition(v) - const trackers = await database.trackers.getByVehicleId(v.uid) - const nearbyVehicleTrackKm: number | null = await VehicleService.getVehicleTrackDistanceKm(v) - if (!nearbyVehicleTrackKm) { - logger.error(`Could not compute track kilometer for vehicle with id ${v.uid} - at track wit id ${v.trackId}`) - return { - id: v.uid, - name: v.name, - track: v.trackId, - type: v.typeId, - trackerIds: trackers.map(t => t.uid), - pos: pos ? { lat: GeoJSONUtils.getLatitude(pos), lng: GeoJSONUtils.getLongitude(pos) } : undefined, - percentagePosition: (await VehicleService.getVehicleTrackDistancePercentage(v)) ?? -1, - heading: await VehicleService.getVehicleHeading(v), - headingTowardsUser: undefined - } - } - const nearbySimplifiedVehicleHeading: number = await VehicleService.getVehicleTrackHeading( - v, - nearbyVehicleTrackKm - ) - return { - id: v.uid, - name: v.name, - track: v.trackId, - type: v.typeId, - trackerIds: trackers.map(t => t.uid), - pos: pos ? { lat: GeoJSONUtils.getLatitude(pos), lng: GeoJSONUtils.getLongitude(pos) } : undefined, - percentagePosition: (await VehicleService.getVehicleTrackDistancePercentage(v)) ?? -1, - heading: await VehicleService.getVehicleHeading(v), - headingTowardsUser: - userVehicleSimplifiedHeading !== 0 && nearbySimplifiedVehicleHeading !== 0 - ? nearbySimplifiedVehicleHeading != userVehicleSimplifiedHeading - : undefined - } - }) - ) - ).filter(v => v.id !== userVehicle.uid && v.track === track.uid && v.percentagePosition !== -1) - - const percentagePositionOnTrack: number | null = await VehicleService.getVehicleTrackDistancePercentage(userVehicle) - if (!percentagePositionOnTrack) { - logger.error(`Could not determine percentage position on track for user with vehicle ${userVehicle.uid}`) - res.sendStatus(500) - return - } - const ret: UpdateResponseApp = { - pos: position, - heading: heading, - vehiclesNearUser: appVehiclesNearUser, - speed: await VehicleService.getVehicleSpeed(userVehicle), - percentagePositionOnTrack: percentagePositionOnTrack, - passingPosition: undefined // TODO: Find out passingPosition - } - res.json(ret) - return - } } diff --git a/Server/src/services/crypto.service.ts b/Server/src/services/crypto.service.ts index 9657bfdf..1bbe1c07 100644 --- a/Server/src/services/crypto.service.ts +++ b/Server/src/services/crypto.service.ts @@ -1,6 +1,13 @@ import * as argon from "argon2" export default class CryptoService { + + /** + * Wrapper method for verifying a plain password against hashed password using argon2. + * + * @param hashedPassword The argon2-hashed password from the database to verify against. + * @param plainPassword The plain password the user entered. + */ public static async verify(hashedPassword: string, plainPassword: string): Promise { let isCorrectPassword: boolean try { @@ -10,10 +17,11 @@ export default class CryptoService { } return isCorrectPassword } + /** * Produces a hash using the argon hashing. * @param input The password, that needs to be hashed - * @returns Undefined, if the hashing is unsuccessful, a hash of the password otherwise. + * @returns undefined, if the hashing is unsuccessful, a hash of the password otherwise. */ public static async produceHash(input: string): Promise { try { diff --git a/Server/src/services/db/poi.controller.ts b/Server/src/services/db/poi.controller.ts index ce93ad65..b19b080e 100644 --- a/Server/src/services/db/poi.controller.ts +++ b/Server/src/services/db/poi.controller.ts @@ -1,230 +1,230 @@ -import { POI, POIType, PrismaClient, Prisma } from "@prisma/client" - -/** - * POIController class - * - * Handles point of interest (POI) specific access to the database. - * This controller handles therefore POIs and POITypes. - * @functions for POITypes: - * - saveType() - * - updateType() - * - removeType() - * - getAllTypes() - * - getTypeById() - * - getTypeByName() - * - * @functions for POIs: - * - save() - * - update() - * - remove() - * - getAll() - * - getById() - * - getByName() - * - */ -export default class POIController { - constructor(private prisma: PrismaClient) {} - - // ========================================================= // - // [POI Types] - - /** - * Saves a type for POIs in the database. - * - * The parameter are given via object deconstruction from the model `POIType`! - * Currently given parameters are: - * @param name - **unique** name of the type of poi. - * @param icon - unique icon name for visualization - * @param description - an optional description for the type of poi. - * @returns POIType - */ - public async saveType(args: Prisma.POITypeCreateInput): Promise { - return await this.prisma.pOIType.create({ - data: args - }) - } - - /** - * Updates a type of poi in the database. - * - * @param uid - Indicator which type should be updated. - * - * The parameter are given via object deconstruction from the model `POIType`! - * Currently given parameters are: - * @param name - New name after change. (Optional) - * @param icon - New unique icon name for visualization after change. (Optional) - * @param description - New description after change. (Optional) - * @returns POIType | null if an error occurs. - */ - public async updateType(uid: number, args: Prisma.POITypeUpdateInput): Promise { - return await this.prisma.pOIType.update({ - where: { - uid: uid - }, - data: args - }) - } - - /** - * Removes a poi type from the database. - * - * @param uid - Indicator which type should be removed. - * @returns True if the removal was successful. Otherwise throws an Error. - */ - public async removeType(uid: number): Promise { - await this.prisma.pOIType.delete({ - where: { - uid: uid - } - }) - return true - } - - /** - * Returns a list of all existing types of poi. - * - * @returns `POIType[]` - List of all types of poi. - */ - public async getAllTypes(): Promise { - return await this.prisma.pOIType.findMany({}) - } - - /** - * Looks up a type given by its uid. - * - * @param uid - Indicator which type should be searched for. - * @returns POIType | null depending on if the type could be found. - */ - public async getTypeById(uid: number): Promise { - return await this.prisma.pOIType.findUnique({ - where: { - uid: uid - } - }) - } - - /** - * Looks up a type given by its name. - * - * @param name - Indicator which type should be searched for. - * @returns POIType | null depending on if the type could be found. - */ - public async getTypeByName(name: string): Promise { - return await this.prisma.pOIType.findUnique({ - where: { - name: name - } - }) - } - - // ========================================================= // - // [POI] - - /** - * Saves a point of interest (POI) in the database. - * - * The parameter are given via object deconstruction from the model `POI`! - * Currently given parameters are: - * @param name - **unique** name of the POI - * @param typeId - POIType Identifier: Maps a POIType to said POI in the database - * @param trackId - Track Identifier : Maps a Track to said POI in the database - * @param position - Coordinates to pinpoint the location of said POI. - * @param description - optional description of said POI - * @param isTurningPoint - optional indicator whether it is possible to turn a vehicle around at this POI - * @returns POI - */ - public async save(args: Prisma.POIUncheckedCreateInput): Promise { - // POIUncheckCreateInput is used because of required relations based on the model! - return await this.prisma.pOI.create({ - data: args - }) - } - - /** - * Updates a POI in the database. - * - * @param uid - Indicator which poi should be updated. - * - * The parameter are given via object deconstruction from the model `POI`! - * Currently given parameters are: - * @param name - New name after change. (Optional) - * @param description - New description after change. (Optional) - * @param typeId - New typeId after change. (Optional) - * @param trackId - New trackId after change. (Optional) - * @param position - New position after change. (Optional) - * @param isTurningPoint - indicator whether it is possible to turn a vehicle around at this POI (Optional) - * @returns POI - */ - public async update(uid: number, args: Prisma.POIUncheckedUpdateInput): Promise { - // POIUncheckUpdateInput is used because of required relations based on the model - return await this.prisma.pOI.update({ - where: { - uid: uid - }, - data: args - }) - } - - /** - * Removes an poi from the database. - * - * @param uid - Indicator which poi should be removed. - * @returns True if the removal was successful. Otherwise throws an Error. - */ - public async remove(uid: number): Promise { - await this.prisma.pOI.delete({ - where: { - uid: uid - } - }) - return true - } - - /** - * Returns a list of all existing pois. - * - * @param trackId - Indicator for filtering all pois for a specific track (Optional) - * @returns POI[] - List of all pois. If an trackId was given: List of all pois on this specific track. - */ - public async getAll(trackId?: number): Promise { - return await this.prisma.pOI.findMany({ - where: { - trackId: trackId - } - }) - } - - /** - * Looks up a poi given by its uid. - * - * @param uid - Indicator which poi should be searched for - * @returns POI | null depending on if the poi could be found. - */ - public async getById(uid: number): Promise { - return await this.prisma.pOI.findUnique({ - where: { - uid: uid - }, - include: { - type: true, - track: true - } - }) - } - - /** - * Looks up pois given by its name. - * - * @param name - Indicator which pois should be searched for - * @param trackId - optional filter indicator to filter for a given track. - * @returns POI[] - List of all pois with the given name. If an trackId was given: List of all pois on this specific track with the given name. - */ - public async getByName(name: string, trackId?: number): Promise { - return await this.prisma.pOI.findMany({ - where: { - name: name, - trackId: trackId - } - }) - } -} +import { POI, POIType, PrismaClient, Prisma } from "@prisma/client" + +/** + * POIController class + * + * Handles point of interest (POI) specific access to the database. + * This controller handles therefore POIs and POITypes. + * @functions for POITypes: + * - saveType() + * - updateType() + * - removeType() + * - getAllTypes() + * - getTypeById() + * - getTypeByName() + * + * @functions for POIs: + * - save() + * - update() + * - remove() + * - getAll() + * - getById() + * - getByName() + * + */ +export default class POIController { + constructor(private prisma: PrismaClient) {} + + // ========================================================= // + // [POI Types] + + /** + * Saves a type for POIs in the database. + * + * The parameter are given via object deconstruction from the model `POIType`! + * Currently given parameters are: + * @param name - **unique** name of the type of poi. + * @param icon - unique icon name for visualization + * @param description - an optional description for the type of poi. + * @returns POIType + */ + public async saveType(args: Prisma.POITypeCreateInput): Promise { + return await this.prisma.pOIType.create({ + data: args + }) + } + + /** + * Updates a type of poi in the database. + * + * @param uid - Indicator which type should be updated. + * + * The parameter are given via object deconstruction from the model `POIType`! + * Currently given parameters are: + * @param name - New name after change. (Optional) + * @param icon - New unique icon name for visualization after change. (Optional) + * @param description - New description after change. (Optional) + * @returns POIType | null if an error occurs. + */ + public async updateType(uid: number, args: Prisma.POITypeUpdateInput): Promise { + return await this.prisma.pOIType.update({ + where: { + uid: uid + }, + data: args + }) + } + + /** + * Removes a poi type from the database. + * + * @param uid - Indicator which type should be removed. + * @returns True if the removal was successful. Otherwise throws an Error. + */ + public async removeType(uid: number): Promise { + await this.prisma.pOIType.delete({ + where: { + uid: uid + } + }) + return true + } + + /** + * Returns a list of all existing types of poi. + * + * @returns `POIType[]` - List of all types of poi. + */ + public async getAllTypes(): Promise { + return await this.prisma.pOIType.findMany({}) + } + + /** + * Looks up a type given by its uid. + * + * @param uid - Indicator which type should be searched for. + * @returns POIType | null depending on if the type could be found. + */ + public async getTypeById(uid: number): Promise { + return await this.prisma.pOIType.findUnique({ + where: { + uid: uid + } + }) + } + + /** + * Looks up a type given by its name. + * + * @param name - Indicator which type should be searched for. + * @returns POIType | null depending on if the type could be found. + */ + public async getTypeByName(name: string): Promise { + return await this.prisma.pOIType.findUnique({ + where: { + name: name + } + }) + } + + // ========================================================= // + // [POI] + + /** + * Saves a point of interest (POI) in the database. + * + * The parameter are given via object deconstruction from the model `POI`! + * Currently given parameters are: + * @param name - **unique** name of the POI + * @param typeId - POIType Identifier: Maps a POIType to said POI in the database + * @param trackId - Track Identifier : Maps a Track to said POI in the database + * @param position - Coordinates to pinpoint the location of said POI. + * @param description - optional description of said POI + * @param isTurningPoint - optional indicator whether it is possible to turn a vehicle around at this POI + * @returns POI + */ + public async save(args: Prisma.POIUncheckedCreateInput): Promise { + // POIUncheckCreateInput is used because of required relations based on the model! + return await this.prisma.pOI.create({ + data: args + }) + } + + /** + * Updates a POI in the database. + * + * @param uid - Indicator which poi should be updated. + * + * The parameter are given via object deconstruction from the model `POI`! + * Currently given parameters are: + * @param name - New name after change. (Optional) + * @param description - New description after change. (Optional) + * @param typeId - New typeId after change. (Optional) + * @param trackId - New trackId after change. (Optional) + * @param position - New position after change. (Optional) + * @param isTurningPoint - indicator whether it is possible to turn a vehicle around at this POI (Optional) + * @returns POI + */ + public async update(uid: number, args: Prisma.POIUncheckedUpdateInput): Promise { + // POIUncheckUpdateInput is used because of required relations based on the model + return await this.prisma.pOI.update({ + where: { + uid: uid + }, + data: args + }) + } + + /** + * Removes an poi from the database. + * + * @param uid - Indicator which poi should be removed. + * @returns True if the removal was successful. Otherwise throws an Error. + */ + public async remove(uid: number): Promise { + await this.prisma.pOI.delete({ + where: { + uid: uid + } + }) + return true + } + + /** + * Returns a list of all existing pois. + * + * @param trackId - Indicator for filtering all pois for a specific track (Optional) + * @returns POI[] - List of all pois. If an trackId was given: List of all pois on this specific track. + */ + public async getAll(trackId?: number): Promise { + return await this.prisma.pOI.findMany({ + where: { + trackId: trackId + } + }) + } + + /** + * Looks up a poi given by its uid. + * + * @param uid - Indicator which poi should be searched for + * @returns POI | null depending on if the poi could be found. + */ + public async getById(uid: number): Promise { + return await this.prisma.pOI.findUnique({ + where: { + uid: uid + }, + include: { + type: true, + track: true + } + }) + } + + /** + * Looks up pois given by its name. + * + * @param name - Indicator which pois should be searched for + * @param trackId - optional filter indicator to filter for a given track. + * @returns POI[] - List of all pois with the given name. If an trackId was given: List of all pois on this specific track with the given name. + */ + public async getByName(name: string, trackId?: number): Promise { + return await this.prisma.pOI.findMany({ + where: { + name: name, + trackId: trackId + } + }) + } +} diff --git a/Server/src/services/poi.service.ts b/Server/src/services/poi.service.ts index 3c76192f..65dc0aae 100644 --- a/Server/src/services/poi.service.ts +++ b/Server/src/services/poi.service.ts @@ -1,9 +1,7 @@ -import { POI, POIType, Prisma, Track, Vehicle } from ".prisma/client" +import { POI, POIType, Prisma, Track } from ".prisma/client" import database from "./database.service" import TrackService from "./track.service" -import VehicleService from "./vehicle.service" import GeoJSONUtils from "../utils/geojsonUtils" - import { logger } from "../utils/logger" /** @@ -33,6 +31,7 @@ export default class POIService { if (track == null) { const tempTrack = await TrackService.getClosestTrack(position) if (tempTrack == null) { + logger.error(`No closest track was found for position ${JSON.stringify(position)}.`) return null } track = tempTrack @@ -41,6 +40,7 @@ export default class POIService { // add kilometer value const enrichedPoint = await this.enrichPOIPosition(position, track) if (enrichedPoint == null) { + logger.error(`The position ${JSON.stringify(position)} could not be enriched.`) return null } @@ -70,7 +70,7 @@ export default class POIService { if (track == null) { const tempTrack = await TrackService.getClosestTrack(point) if (tempTrack == null) { - // TODO: log this + logger.error(`No closest track was found for position ${JSON.stringify(point)}.`) return null } track = tempTrack @@ -79,22 +79,13 @@ export default class POIService { // calculate and set track kilometer const trackKm = await TrackService.getPointTrackKm(point, track) if (trackKm == null) { - // TODO: log this + logger.error(`Could not get track distance for position ${JSON.stringify(point)} on track with id ${track.uid}.`) return null } GeoJSONUtils.setTrackKm(point, trackKm) return point } - /** - * - * @param id id of POI to search for - * @returns `POI` with `id` if it exists, `null` otherwise - */ - public static async getPOIById(id: number): Promise { - return database.pois.getById(id) - } - /** * Wrapper to get distance of poi in kilometers along the assigned track * @param poi `POI` to get the distance for @@ -104,7 +95,7 @@ export default class POIService { // get closest track if none is given const poiPos = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) if (poiPos == null) { - // TODO: log this + logger.error(`Position ${JSON.stringify(poi.position)} could not be parsed.`) return null } @@ -117,19 +108,21 @@ export default class POIService { // Therefore, obtain and typecast the position const poiPos = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) if (poiPos == null) { + logger.error(`Position ${JSON.stringify(poi.position)} could not be parsed.`) return null } // get track of POI to enrich it const track = await database.tracks.getById(poi.trackId) if (track == null) { + logger.error(`Track with id ${poi.trackId} was not found.`) return null } // then enrich it with the given track const enrichedPos = await this.enrichPOIPosition(poiPos, track) if (enrichedPos == null) { - logger.error(`Could not enrich position of POI with ID ${poi.uid}`) + logger.error(`Could not enrich position of POI with ID ${poi.uid}.`) return null } // try to update the poi in the database, now that we have enriched it @@ -142,7 +135,7 @@ export default class POIService { // and re-calculate poiTrackKm (we do not care that much at this point if the update was successful) poiTrackKm = GeoJSONUtils.getTrackKm(enrichedPos) if (poiTrackKm == null) { - logger.error(`Could not get distance as percentage of POI with ID ${poi.uid}.`) + logger.error(`Could not get track kilometer of POI position ${JSON.stringify(enrichedPos)}.`) return null } } @@ -159,17 +152,19 @@ export default class POIService { // get track length const track = await database.tracks.getById(poi.trackId) if (track == null) { + logger.error(`Track with id ${poi.trackId} was not found.`) return null } const trackLength = TrackService.getTrackLength(track) if (trackLength == null) { + logger.error(`Length of track with id ${track.uid} could not be calculated.`) return null } const poiDistKm = await this.getPOITrackDistanceKm(poi) if (poiDistKm == null) { - logger.error(`Could not get distance as percentage of POI with ID ${poi.uid}.`) + logger.error(`Could not get track kilometer of POI with ID ${poi.uid}.`) return null } @@ -177,132 +172,6 @@ export default class POIService { return (poiDistKm / trackLength) * 100 } - /** - * Search for nearby POI's either within a certain distance or by amount - * @param point point to search nearby POI's from - * @param track `Track` to search on for POIs. If none is given, the closest will be used. - * @param count amount of points, that should be returned. If none given only one (i.e. the nearest) will be returned. - * @param heading could be either 1 or -1 to search for POI only towards the end and start of the track (seen from `point`) respectively - * @param maxDistance maximum distance in track-kilometers to the POI's - * @param type `POIType` to filter the returned POI's by - * @returns `POI[]`, either #`count` of nearest POI's or all POI's within `maxDistance` of track-kilometers, but at most #`count`. - * That is the array could be empty. - */ - public static async getNearbyPOIs( - point: GeoJSON.Feature | Vehicle, - track?: Track, - count?: number, - heading?: number, - maxDistance?: number, - type?: POIType - ): Promise { - // TODO: testing - // TODO: just copied from VehicleService, i.e. there is probably a better solution - // extract vehicle position if a vehicle is given instead of a point - if ((point).uid) { - // also use the assigned track if none is given - if (track == null) { - const tempTrack = await database.tracks.getById((point).trackId) - if (tempTrack == null) { - return null - } - track = tempTrack - } - - const vehiclePosition = await VehicleService.getVehiclePosition(point) - if (vehiclePosition == null) { - return null - } - point = vehiclePosition - } - - // now we can safely assume, that this is actually a point - const searchPoint = >point - // check if a track is given, else initialize it with the closest one - if (track == null) { - const tempTrack = await TrackService.getClosestTrack(searchPoint) - if (tempTrack == null) { - // TODO: log this - return null - } - track = tempTrack - } - - // compute distance of point mapped on track - const trackDistance = await TrackService.getPointTrackKm(searchPoint, track) - if (trackDistance == null) { - // TODO: log this - return null - } - - // search for all POIs on the track - let allPOIsForTrack = await this.getAllPOIsForTrack(track, type) - - // filter pois by heading if given - if (heading != null) { - // invalid heading - if (heading != 1 && heading != -1) { - // TODO: log this - return null - } - - allPOIsForTrack.filter(async function (poi, _index, _pois) { - const poiTrackKm = await POIService.getPOITrackDistanceKm(poi) - if (poiTrackKm == null) { - return false - } - return poiTrackKm - trackDistance * heading > 0 - }) - } - - // filter pois by distance if given - if (maxDistance != null) { - allPOIsForTrack.filter(async function (poi, _index, _pois) { - const poiTrackKm = await POIService.getPOITrackDistanceKm(poi) - if (poiTrackKm == null) { - return false - } - // consider both directions (heading would filter those out) - return Math.abs(poiTrackKm - trackDistance) < maxDistance - }) - } - // sort POI's by distance to searched point - allPOIsForTrack = allPOIsForTrack.sort(function (poi0, poi1) { - // parse POI position - const POIPos0 = GeoJSONUtils.parseGeoJSONFeaturePoint(poi0.position) - const POIPos1 = GeoJSONUtils.parseGeoJSONFeaturePoint(poi1.position) - if (POIPos0 == null || POIPos1 == null) { - // TODO: log this - return 0 - } - - // if this happens, we cannot sort the POI's - const POIPos0TrackKm = GeoJSONUtils.getTrackKm(POIPos0) - const POIPos1TrackKm = GeoJSONUtils.getTrackKm(POIPos1) - if (POIPos0TrackKm == null || POIPos1TrackKm == null) { - // TODO: log this, maybe some other handling - return 0 - } - - // compute distances to vehicle and compare - const distanceToVehicle0 = Math.abs(POIPos0TrackKm - trackDistance) - const distanceToVehicle1 = Math.abs(POIPos1TrackKm - trackDistance) - return distanceToVehicle0 - distanceToVehicle1 - }) - - // check if a certain amount is searched for - count = count == null ? 1 : count - - // if less POI's were found then we need to return, we return every POI that we have - if (count > allPOIsForTrack.length) { - return allPOIsForTrack - } - - // only return first #count of POI's - allPOIsForTrack.slice(0, count) - return allPOIsForTrack - } - /** * Search for POI's on a track * @param track `Track` to search on for POI's @@ -316,179 +185,10 @@ export default class POIService { } // filter by type - const trackPOIs = await database.pois.getAll(track.uid) - trackPOIs.filter(function (poi, _index, _poiList) { + let trackPOIs = await database.pois.getAll(track.uid) + trackPOIs = trackPOIs.filter(function (poi, _index, _poiList) { return poi.typeId == type.uid }) return trackPOIs } - - /** - * Set a new position for an existing POI - * @param poi `POI` to update - * @param position new position of `poi` - * @returns updated `POI` if successful, `null` otherwise - */ - public static async setPOIPosition(poi: POI, position: GeoJSON.Feature): Promise { - // enrich and update - const POITrack = await database.tracks.getById(poi.trackId) - if (POITrack == null) { - // TODO: this really should not happen, how to handle? delete POI? - return null - } - const enrichedPoint = await this.enrichPOIPosition(position, POITrack) - if (enrichedPoint == null) { - return null - } - - // Note: Based on Feature it is not possible to cast to Prisma.InputJsonValue directly - // Therefore we cast it into unknown first. (Also recommended by Prisma itself) - return database.pois.update(poi.uid, { position: enrichedPoint as unknown as Prisma.InputJsonValue }) - } - - /** - * Rename an existing POI - * @param poi `POI` to rename - * @param newName new name of `poi` - * @returns renamed `POI` if successful, `null` otherwise - */ - public static async renamePOI(poi: POI, newName: string): Promise { - return database.pois.update(poi.uid, { name: newName }) - } - - /** - * Update description for a given POI - * @param poi `POI` to update description for - * @param newDesc new description for `poi` - * @returns updated `POI` if successful, `null` otherwise - */ - public static async updateDescription(poi: POI, newDesc: string): Promise { - return database.pois.update(poi.uid, { description: newDesc }) - } - - /** - * Set new type of POI - * @param poi `POI` to update - * @param type new type of `poi` - * @returns updated `POI` if successful, `null` otherwise - */ - public static async setPOIType(poi: POI, type: POIType): Promise { - return database.pois.update(poi.uid, { typeId: type.uid }) - } - - /** - * Set track for POI - * @param poi `POI` to set track for - * @param track new `Track` for `poi` - * @returns updated `POI` if successful, `null` otherwise - */ - public static async setPOITrack(poi: POI, track: Track): Promise { - // update track kilometer value first - const poiPos = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) - if (poiPos == null) { - // TODO: log this - return null - } - const updatedPOIPos = await this.enrichPOIPosition(poiPos, track) - if (updatedPOIPos == null) { - return null - } - - // update poi's track and track kilometer - - // Note: Based on Feature it is not possible to cast to Prisma.InputJsonValue directly - // Therefore we cast it into unknown first. (Also recommended by Prisma itself) - return database.pois.update(poi.uid, { - trackId: track.uid, - position: updatedPOIPos as unknown as Prisma.InputJsonValue - }) - } - - /** - * Set if a POI is a turning point - * @param poi `POI` to update - * @param isTurningPoint indicator if `poi` is a turning point - * @returns updated `POI` if successful, `null` otherwise - */ - public static async setTurningPoint(poi: POI, isTurningPoint: boolean): Promise { - return database.pois.update(poi.uid, { isTurningPoint }) - } - - /** - * Delete existing POI - * @param poi `POI` to delete - * @returns `true`, if deletion was successful, `false` otherwise - */ - public static async removePOI(poi: POI): Promise { - return database.pois.remove(poi.uid) - } - - // --- POI-types --- - - /** - * Create new POI-type - * @param type name of new POI-type - * @param icon name of an icon associated to type - * @param desc optional description of new POI-type - * @returns created `POIType` if successful, `null` otherwise - */ - public static async createPOIType(type: string, icon: string, desc?: string): Promise { - return database.pois.saveType({ name: type, icon, description: desc }) - } - - /** - * - * @returns all existing `POIType`s - */ - public static async getAllPOITypes(): Promise { - return database.pois.getAllTypes() - } - - /** - * Search for POI type by a given id - * @param id id to search POI type by - * @returns `POIType` with id `id` if successful, `null` otherwise - */ - public static async getPOITypeById(id: number): Promise { - return database.pois.getTypeById(id) - } - - /** - * Change name of existing POI-type - * @param type `POIType` to change name of - * @param newType new name for `type` - * @returns renamed `POIType` if successful, `null` otherwise - */ - public static async renamePOIType(type: POIType, newType: string): Promise { - return database.pois.updateType(type.uid, { name: newType }) - } - - /** - * Update description of existing POI-type - * @param type `POIType` to change description of - * @param desc new description for `type` - * @returns updated `POIType` if successful, `null` otherwise - */ - public static async setPOITypeDescription(type: POIType, desc: string): Promise { - return database.pois.updateType(type.uid, { description: desc }) - } - - /** - * Change icon of POI type - * @param type `POIType` to change the icon of - * @param icon name of new icon to be associated with type - * @returns updated `POI` if successful, `null` otherwise - */ - public static async setPOITypeIcon(type: POIType, icon: string): Promise { - return database.pois.updateType(type.uid, { icon }) - } - - /** - * Delete existing POI-type - * @param type `POIType` to delete - * @returns `true` if deletion was successful, `false` otherwise - */ - public static async removePOIType(type: POIType): Promise { - return database.pois.removeType(type.uid) - } } diff --git a/Server/src/services/track.service.ts b/Server/src/services/track.service.ts index 8fb956db..74f5bb5d 100644 --- a/Server/src/services/track.service.ts +++ b/Server/src/services/track.service.ts @@ -3,10 +3,11 @@ import database from "./database.service" import GeoJSONUtils from "../utils/geojsonUtils" import distance from "@turf/distance" -import nearestPointOnLine from "@turf/nearest-point-on-line" +import nearestPointOnLine, { NearestPointOnLine } from "@turf/nearest-point-on-line" import * as turfMeta from "@turf/meta" import * as turfHelpers from "@turf/helpers" import bearing from "@turf/bearing" +import { logger } from "../utils/logger" /** * Service for track management. This also includes handling the GeoJSON track data. @@ -65,7 +66,7 @@ export default class TrackService { track: GeoJSON.FeatureCollection ): GeoJSON.FeatureCollection { // iterate over all features - turfMeta.featureEach(track, function (feature, featureIndex) { + turfMeta.featureEach(track, function(feature, featureIndex) { // compute track kilometer for each point if (featureIndex > 0) { const prevFeature = track.features[featureIndex - 1] @@ -92,6 +93,7 @@ export default class TrackService { // get the track kilometer value from projected point const projectedPoint = await this.getProjectedPointOnTrack(position, track) if (projectedPoint == null) { + logger.error(`Could not project position ${JSON.stringify(position)}.`) return null } return GeoJSONUtils.getTrackKm(projectedPoint) @@ -113,6 +115,7 @@ export default class TrackService { // if an error occured while trying to find the closest track, there is nothing we can do if (tempTrack == null) { + logger.error(`Could not find closest track for position ${JSON.stringify(position)}.`) return null } track = tempTrack @@ -121,18 +124,22 @@ export default class TrackService { // converting feature collection of points from track to linestring to project position onto it const trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(track.data) if (trackData == null) { - // TODO: log this + logger.error(`Could not parse track data of track with id ${track.uid}.`) return null } const lineStringData: GeoJSON.Feature = turfHelpers.lineString(turfMeta.coordAll(trackData)) // projecting point on linestring of track // this also computes on which line segment this point is, the distance to position and the distance along the track - const projectedPoint = nearestPointOnLine(lineStringData, position) + const projectedPoint: NearestPointOnLine = nearestPointOnLine(lineStringData, position) // for easier access we set the property of track kilometer to the already calculated value if (projectedPoint.properties["location"] == null) { - // TODO: log this + logger.error( + `Turf error: Could not calculate nearest point on line correctly for position ${JSON.stringify( + position + )} and for linestring of track with id ${track.uid}.` + ) // this is a slight overreaction as we can still return the projected point, but the track kilometer property will not be accessible return null } @@ -152,17 +159,20 @@ export default class TrackService { // validate track kilometer value const trackLength = this.getTrackLength(track) if (trackLength == null) { - // TODO: log this + logger.error(`Length of track with id ${track.uid} could not be calculated.`) return null } if (trackKm < 0 || trackKm > trackLength) { + logger.error( + `Unexpected value for track kilometer: ${trackKm}. This needs to be more than 0 and less than ${trackLength}.` + ) return null } // get track data const trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(track.data) if (trackData == null) { - // TODO: log this + logger.error(`Could not parse track data of track with id ${track.uid}.`) return null } @@ -176,7 +186,7 @@ export default class TrackService { const trackPoint = trackData.features[i] const trackPointKm = GeoJSONUtils.getTrackKm(trackPoint) if (trackPointKm == null) { - // TODO: log this, this should not happen + logger.error(`Could not access track kilometer value of track point ${i} of track with id ${track.uid}.`) return null } @@ -186,7 +196,9 @@ export default class TrackService { } } - // TODO: log this, this would be really weird as we validated the track kilometer value passed + logger.error( + `Track kilometer value ${trackKm} could not be found while iterating track points of track with id ${track.uid}.` + ) return null } @@ -199,6 +211,7 @@ export default class TrackService { const tracks = await database.tracks.getAll() // there are no tracks at all if (tracks.length == 0) { + logger.warn(`No track was found.`) return null } @@ -208,16 +221,20 @@ export default class TrackService { for (let i = 0; i < tracks.length; i++) { const trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(tracks[i].data) if (trackData == null) { - // TODO: log this + logger.error(`Could not parse track data of track with id ${tracks[i].uid}.`) return null } // converting feature collection of points to linestring to measure distance const lineStringData: GeoJSON.Feature = turfHelpers.lineString(turfMeta.coordAll(trackData)) // this gives us the nearest point on the linestring including the distance to that point - const closestPoint: GeoJSON.Feature = nearestPointOnLine(lineStringData, position) - if (closestPoint.properties == null || closestPoint.properties["dist"] == null) { - // TODO: this should not happen, so maybe log this + const closestPoint: NearestPointOnLine = nearestPointOnLine(lineStringData, position) + if (closestPoint.properties["dist"] == null) { + logger.warn( + `Turf error: Could not calculate nearest point on line correctly for position ${JSON.stringify( + position + )} and for linestring of track with id ${tracks[i].uid}.` + ) continue } @@ -230,6 +247,7 @@ export default class TrackService { // check if closest track was found if (minTrack < 0) { + logger.warn(`Somehow no closest track was found even after iterating all existing tracks.`) return null } else { return tracks[minTrack] @@ -246,7 +264,7 @@ export default class TrackService { // load track data const trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(track.data) if (trackData == null) { - // TODO: log this + logger.error(`Could not parse track data of track with id ${track.uid}.`) return null } @@ -254,7 +272,7 @@ export default class TrackService { const trackPointsLength = trackData.features.length const trackLength = GeoJSONUtils.getTrackKm(trackData.features[trackPointsLength - 1]) if (trackLength == null) { - // TODO: log this, track data invalid, probably check if track exists and try to get it by id + logger.error(`Could not access track kilometer value of last track point of track with id ${track.uid}.`) return null } return trackLength @@ -268,64 +286,9 @@ export default class TrackService { public static getTrackAsLineString(track: Track): GeoJSON.Feature | null { const trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(track.data) if (trackData == null) { - // TODO: log this + logger.error(`Could not parse track data of track with id ${track.uid}.`) return null } return turfHelpers.lineString(turfMeta.coordAll(trackData)) } - - /** - * Search for all tracks that have a given location as start or end point - * @param location location to search for - * @returns all `Track[]`, which have `location` either as their starting location or as their destination, thus could be empty - */ - public static async searchTrackByLocation(location: string): Promise { - return database.tracks.getByLocation(location) - } - - /** - * Assign a new path of GeoJSON points to an existing track - * @param track existing track - * @param path new path for `track` - * @returns `Track` with updated path - */ - public static async updateTrackPath( - track: Track, - path: GeoJSON.FeatureCollection - ): Promise { - const enrichedTrack = await this.enrichTrackData(path) - - // Note: Based on FeatureCollection it is not possible to cast to Prisma.InputJsonValue directly - // Therefore we cast it into unknown first. (Also recommended by Prisma itself) - return database.tracks.update(track.uid, { data: enrichedTrack as unknown as Prisma.InputJsonValue }) - } - - /** - * Update starting location of a track - * @param track `Track` to update - * @param newStart new starting location of `track` - * @returns updated `Track` if successful, `null` otherwise - */ - public static async setStart(track: Track, newStart: string): Promise { - return database.tracks.update(track.uid, { start: newStart }) - } - - /** - * Update destination of a track - * @param track `Track` to update - * @param newDest new destination of `track` - * @returns updated `Track` if successful, `null` otherwise - */ - public static async setDestination(track: Track, newDest: string): Promise { - return database.tracks.update(track.uid, { stop: newDest }) - } - - /** - * Delete track - * @param track track to delete - * @returns `true` if deletion was successfull, `false` otherwise - */ - public static async removeTrack(track: Track): Promise { - return database.tracks.remove(track.uid) - } } diff --git a/Server/src/services/tracker.service.ts b/Server/src/services/tracker.service.ts index 4d326231..6d3a77d4 100644 --- a/Server/src/services/tracker.service.ts +++ b/Server/src/services/tracker.service.ts @@ -1,64 +1,10 @@ -import { logger } from "../utils/logger" -import { Log, Prisma, Tracker, Vehicle } from "@prisma/client" -import VehicleService from "./vehicle.service" +import { Log, Prisma, Vehicle } from "@prisma/client" import database from "./database.service" /** * Service for tracker management. This includes registration of new trackers and writing logs. */ export default class TrackerService { - /** - * Register new trackers - * @param trackerId id of `Tracker` - * @param data data from tracker when sending hello-message - * @returns `Tracker` if registration was successful, `null` otherwise - */ - public static async registerTracker(trackerId: string, data?: unknown): Promise { - const tracker = await this.getTrackerById(trackerId) - if (tracker == null) { - return await database.trackers.save({ uid: trackerId, data: data as Prisma.InputJsonValue, vehicleId: null }) - } else { - return tracker - } - } - - /** - * Search for tracker by id - * @param id id of `Tracker` - * @returns `Tracker` if it exists, `null` otherwise - */ - public static async getTrackerById(id: string): Promise { - return database.trackers.getById(id) - } - - /** - * Get all trackers for a given vehicle - * @param vehicleId `Vehicle.uid`, the trackers are assigned to - * @returns `Tracker`[] assigned to `vehicle` - */ - public static async getTrackerByVehicle(vehicleId: number): Promise { - return await database.trackers.getByVehicleId(vehicleId) - } - - /** - * Assign tracker to a vehicle - * @param tracker `Tracker` to assign to a vehicle - * @param vehicle `Vehicle`, which gets assigned a tracker - * @returns `Tracker` that got assigned to a `Vehicle` if successful, `null` otherwise - */ - public static async setVehicle(tracker: Tracker, vehicle: Vehicle): Promise { - return database.trackers.update(tracker.uid, { vehicleId: vehicle.uid }) - } - - /** - * Deletes a tracker - * @param tracker `Tracker` to delete - * @returns `true` if deletion was successful, `false` otherwise - */ - public static async removeTracker(tracker: Tracker): Promise { - return database.trackers.remove(tracker.uid) - } - // --- Vehicle logs --- /** @@ -100,61 +46,4 @@ export default class TrackerService { trackerId }) } - - /** - * TODO: Define internal schema for data? Where? - * Log new data received by a tracker (wrapper to call from tracker endpoints, - * because they cannot "know" what vehicle they are on) - * @param trackerId id of the `Tracker´ - * @param timestamp creation timestamp of the log - * @param position current position - * @param heading heading of the vehicle in degree (0-359) - * @param speed speed of the vehicle in kmph - * @param battery battery voltage of the tracker in V - * @param data data received by a tracker - * @returns a new entry `Log` if successful, `null` otherwise - */ - public static async appendTrackerLog( - trackerId: string, - timestamp: Date, - position: [number, number], - heading: number, - speed: number, - battery: number, - data: unknown - ): Promise { - logger.info("reached service") - logger.info(data) - - // check if tracker already exists and if not create it - let tracker = await this.getTrackerById(trackerId) - if (tracker == null) { - tracker = await this.registerTracker(trackerId) - } - - if (tracker == null || tracker.vehicleId == null) { - // TODO: log this, especially if tracker is still null - // (no vehicle id is not that critical as a tracker could exist without an assigned vehicle, - // but logging will not happen then and would not make sense) - return null - } - - const vehicle = await VehicleService.getVehicleById(tracker.vehicleId) - if (vehicle == null) { - // TODO: log this, a vehicle should exist if a tracker is assigned to it - return null - } - // actual wrapper - return this.appendLog(vehicle, timestamp, position, heading, speed, trackerId, battery, data) - } - - /** - * Get log entries for a given vehicle - * @param vehicle `Vehicle` to search the log entries by - * @param tracker (optional) `Tracker` to filter logs - * @returns `Log[]` of all log entries for `vehicle` or `null` if an error occured - */ - public static async getVehicleLogs(vehicle: Vehicle, tracker?: Tracker): Promise { - return database.logs.getAll(vehicle.uid, tracker?.uid) - } } diff --git a/Server/src/services/vehicle.service.ts b/Server/src/services/vehicle.service.ts index 73b3e0de..099c80d9 100644 --- a/Server/src/services/vehicle.service.ts +++ b/Server/src/services/vehicle.service.ts @@ -1,8 +1,7 @@ import { logger } from "../utils/logger" import database from "./database.service" -import { Vehicle, VehicleType, Track, Tracker, Log } from ".prisma/client" +import { Log, Track, Vehicle, VehicleType } from ".prisma/client" import TrackService from "./track.service" -import TrackerService from "./tracker.service" import GeoJSONUtils from "../utils/geojsonUtils" import along from "@turf/along" @@ -27,41 +26,12 @@ export default class VehicleService { }) } - /** - * Create a new vehicle - * @param type `VehicleType` of new vehicle - * @param name name for new vehicle (has to be unique for the track) - * @param track_uid `Track` - * @returns created `Vehicle` if successful, `null` otherwise - */ - public static async createVehicle(type: VehicleType, track_uid: number, name: string): Promise { - return database.vehicles.save({ name, typeId: type.uid, trackId: track_uid }) - } - - /** - * Search vehicle by id - * @param id id to search vehicle for - * @returns `Vehicle` with id `id` if it exists, `null` otherwise - */ - public static async getVehicleById(id: number): Promise { - return database.vehicles.getById(id) - } - - /** - * Search vehicle by name (this function should not be used mainly to identify a vehicle, but rather to get the vehicle id) - * @param name name to search the vehicle by (which should be unique on the given track) - * @param track `Track` the vehicle is assigned to - * @returns `Vehicle` with name `name` if it exists, `null` otherwise - */ - public static async getVehicleByName(name: string, track: Track): Promise { - return database.vehicles.getByName(name, track.uid) - } - /** * Search for nearby vehicles either within a certain distance or by amount and either from a given point or vehicle * @param point point to search nearby vehicles from, this could also be a vehicle * * @param track `Track` to search on for vehicles. If none is given and `point` is not a `Vehicle`, the closest will be used. * If none is given and `point` is a `Vehicle`, the assigned track will be used. + * @param track The track the vehicles should be on. * @param count amount of vehicles, that should be returned. If none given only one (i.e. the nearest) will be returned. * @param heading could be either 1 or -1 to search for vehicles only towards the end and start of the track (seen from `point`) respectively * @param maxDistance maximum distance in track-kilometers to the vehicles @@ -69,6 +39,7 @@ export default class VehicleService { * @returns `Vehicle[]` either #`count` of nearest vehicles or all vehicles within `distance` of track-kilometers, but at most #`count`. * That is the array could be empty. `null` if an error occurs */ + // NOT ADDING LOGGING HERE, BECAUSE IT WILL BE REMOVED ANYWAY (see issue #114) public static async getNearbyVehicles( point: GeoJSON.Feature | Vehicle, track?: Track, @@ -126,7 +97,7 @@ export default class VehicleService { return null } - allVehiclesOnTrack.filter(async function (vehicle, _index, _vehicles) { + allVehiclesOnTrack = allVehiclesOnTrack.filter(async function (vehicle, _index, _vehicles) { const vehicleTrackKm = await VehicleService.getVehicleTrackDistanceKm(vehicle) if (vehicleTrackKm == null) { // TODO: log this @@ -138,7 +109,7 @@ export default class VehicleService { // filter vehicles by distance if given if (maxDistance != null) { - allVehiclesOnTrack.filter(async function (vehicle, _index, _vehicles) { + allVehiclesOnTrack = allVehiclesOnTrack.filter(async function (vehicle, _index, _vehicles) { const vehicleTrackKm = await VehicleService.getVehicleTrackDistanceKm(vehicle) if (vehicleTrackKm == null) { return false @@ -182,7 +153,7 @@ export default class VehicleService { } // only return first #count of POI's - allVehiclesOnTrack.slice(0, count) + allVehiclesOnTrack = allVehiclesOnTrack.slice(0, count) return allVehiclesOnTrack } @@ -267,7 +238,7 @@ export default class VehicleService { const lastTrackerLog = trackerLogs[0] const lastTrackerPosition = GeoJSONUtils.parseGeoJSONFeaturePoint(lastTrackerLog.position) if (lastTrackerPosition == null) { - logger.warn(`Position ${trackerLogs[0].position} is not in GeoJSON-format.`) + logger.warn(`Position ${JSON.stringify(trackerLogs[0].position)} is not in GeoJSON-format.`) continue } @@ -324,7 +295,7 @@ export default class VehicleService { // parse position from log const lastPosition = GeoJSONUtils.parseGeoJSONFeaturePoint(log.position) if (lastPosition == null) { - logger.warn(`Position ${log.position} is not in GeoJSON-format.`) + logger.warn(`Position ${JSON.stringify(log.position)} is not in GeoJSON-format.`) continue } @@ -373,7 +344,9 @@ export default class VehicleService { const lastPosition = GeoJSONUtils.parseGeoJSONFeaturePoint(appPositions[i][1].position) if (lastPosition == null) { // at this point this should not happen anymore - logger.error(`Position ${appPositions[i][1].position} is not in GeoJSON-format, but should be.`) + logger.error( + `Position ${JSON.stringify(appPositions[i][1].position)} is not in GeoJSON-format, but should be.` + ) return null } const projectedPoint = nearestPointOnLine(lineStringData, lastPosition) @@ -428,8 +401,6 @@ export default class VehicleService { * @returns the last known position of `vehicle` mapped on its track, null if an error occurs */ private static async getLastKnownVehiclePosition(vehicle: Vehicle): Promise | null> { - // TODO: this could be optimized by computing an average position from all last entries by all trackers - // get last log and track of vehicle const lastLog = await database.logs.getAll(vehicle.uid, undefined, 1) if (lastLog.length != 1) { @@ -463,14 +434,14 @@ export default class VehicleService { // get track point of vehicle const vehicleTrackPoint = await this.getVehiclePosition(vehicle) if (vehicleTrackPoint == null) { - // TODO: log this + logger.error(`Could not compute position of vehicle with id ${vehicle.uid}.`) return null } // get track kilometer for vehicle position const vehicleTrackKm = GeoJSONUtils.getTrackKm(vehicleTrackPoint) if (vehicleTrackKm == null) { - // TODO: log this + logger.error(`Could not read track kilometer value from position ${JSON.stringify(vehicleTrackPoint)}.`) return null } @@ -486,7 +457,7 @@ export default class VehicleService { // get track const track = await database.tracks.getById(vehicle.trackId) if (track == null) { - // TODO: logging + logger.error(`Track with id ${vehicle.trackId} was not found.`) return null } @@ -494,6 +465,9 @@ export default class VehicleService { const trackLength = TrackService.getTrackLength(track) const vehicleDistance = await this.getVehicleTrackDistanceKm(vehicle) if (trackLength == null || vehicleDistance == null) { + logger.error( + `Distance of track with id ${track.uid} or distance of vehicle with id ${vehicle.uid} on that track could not be computed.` + ) return null } @@ -554,7 +528,7 @@ export default class VehicleService { // get track const track = await database.tracks.getById(vehicle.trackId) if (track == null) { - // TODO: log + logger.error(`Track with id ${vehicle.trackId} was not found.`) return 0 } @@ -564,7 +538,7 @@ export default class VehicleService { // finally compute track heading const trackBearing = await TrackService.getTrackHeading(track, trackKm) if (trackBearing == null) { - // TODO: log this + logger.error(`Could not compute heading of track with id ${track.uid} at track kilometer ${trackKm}.`) return 0 } // TODO: maybe give this a buffer of uncertainty @@ -583,6 +557,7 @@ export default class VehicleService { */ public static async getVehicleSpeed(vehicle: Vehicle): Promise { // get all trackers for given vehicle + // TODO: remove necessity of trackers const trackers = await database.trackers.getByVehicleId(vehicle.uid) if (trackers.length == 0) { logger.error(`No tracker found for vehicle ${vehicle.uid}.`) @@ -615,118 +590,4 @@ export default class VehicleService { } return avgSpeed } - - /** - * Rename an existing vehicle - * @param vehicle `Vehicle` to rename - * @param newName new name for `vehicle` - * @returns renamed `Vehicle` if successful, `null` otherwise - */ - public static async renameVehicle(vehicle: Vehicle, newName: string): Promise { - return database.vehicles.update(vehicle.uid, { name: newName }) - } - - /** - * Update type of vehicle - * @param vehicle `Vehicle` to set new type for - * @param type new `VehicleType` of `vehicle` - * @returns updated `Vehicle` if successful, `null` otherwise - */ - public static async setVehicleType(vehicle: Vehicle, type: VehicleType): Promise { - return database.vehicles.update(vehicle.uid, { typeId: type.uid }) - } - - /** - * Assign a new tracker to a given vehicle (wrapper for TrackerService) - * @param vehicle `Vehicle` to assign `tracker` to - * @param tracker `Tracker` to be assigned to `vehicle` - * @returns updated `Tracker` with assigned `vehicle` if successful, `null` otherwise - */ - public static async assignTrackerToVehicle(tracker: Tracker, vehicle: Vehicle): Promise { - return TrackerService.setVehicle(tracker, vehicle) - } - - /** - * Delete existing vehicle - * @param vehicle `Vehicle` to delete - * @returns `true` if deletion was successful, `false` otherwise - */ - public static async removeVehicle(vehicle: Vehicle): Promise { - return database.vehicles.remove(vehicle.uid) - } - - // --- vehicle types --- - - /** - * Create a new vehicle type - * @param type description of new vehicle type - * @param icon name of an icon associated to type - * @param desc (optional) description for new vehicle type - * @returns created `VehicleType` if successful, `null` otherwise - */ - public static async createVehicleType(type: string, icon: string, desc?: string): Promise { - return database.vehicles.saveType({ name: type, icon, description: desc }) - } - - /** - * - * @returns all existing `VehicleType`s - */ - public static async getAllVehicleTypes(): Promise { - return database.vehicles.getAllTypes() - } - - /** - * Search vehicle type by a given id - * @param id id to search vehicle type for - * @returns `VehicleType` with id `id`, null if not successful - */ - public static async getVehicleTypeById(id: number): Promise { - return database.vehicles.getTypeById(id) - } - - /** - * Change name of existing vehicle type - * @param type `VehicleType` to change name of - * @param newType new name for `type` - * @returns updated `VehicleType` if successful, `null` otherwise - */ - public static async renameVehicleType(type: VehicleType, newType: string): Promise { - return database.vehicles.updateType(type.uid, { name: newType }) - } - - /** - * Change description of vehicle type - * @param type `VehicleType` to change the description of - * @param desc new description for `type` - * @returns updated `VehicleType` if successful, `null` otherwise - */ - public static async setVehicleTypeDescription(type: VehicleType, desc: string): Promise { - return database.vehicles.updateType(type.uid, { description: desc }) - } - - /** - * Change icon of vehicle type - * @param type `VehicleType` to change the icon of - * @param icon name of new icon to be associated with type - * @returns updated `VehicleType` if successful, `null` otherwise - */ - public static async setVehicleTypeIcon(type: VehicleType, icon: string): Promise { - return database.vehicles.updateType(type.uid, { icon }) - } - - /** - * Delete existing vehicle type - * @param type `VehicleType` to delete - * @returns `true` if deletion was successful, `false` otherwise - */ - public static async removeVehicleType(type: VehicleType): Promise { - return database.vehicles.remove(type.uid) - } - - static async getAllVehicles() { - const vehicles: Vehicle[] = await database.vehicles.getAll() - - return vehicles - } } diff --git a/Website/package-lock.json b/Website/package-lock.json index 8ea06706..bf2f0fd6 100644 --- a/Website/package-lock.json +++ b/Website/package-lock.json @@ -24,6 +24,7 @@ "react-dom": "18.2.0", "react-select": "^5.7.4", "server-only": "^0.0.1", + "sharp": "^0.32.5", "swr": "^2.2.0", "tailwindcss": "3.3.2" }, @@ -1310,6 +1311,11 @@ "dequal": "^2.0.3" } }, + "node_modules/b4a": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", + "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -1329,6 +1335,25 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1337,6 +1362,16 @@ "node": ">=8" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1388,6 +1423,29 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -1500,16 +1558,32 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1520,8 +1594,16 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "node_modules/commander": { "version": "4.1.1", @@ -1640,6 +1722,28 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1671,6 +1775,14 @@ "node": ">=6" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -1725,6 +1837,14 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.15.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", @@ -2310,12 +2430,25 @@ "node": ">=0.10.0" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -2446,6 +2579,11 @@ "url": "https://www.patreon.com/infusion" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2548,6 +2686,11 @@ "git-format-staged": "git-format-staged" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -2745,6 +2888,25 @@ "react-is": "^16.7.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -2792,6 +2954,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -3321,7 +3488,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -3354,6 +3520,17 @@ "node": ">=8.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -3378,11 +3555,15 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3416,6 +3597,11 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -3491,6 +3677,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-abi": { + "version": "3.47.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.47.0.tgz", + "integrity": "sha512-2s6B2CWZM//kPgwnuI0KrYwNjfdByE25zvAaEpq9IH4zcNsarH8Ihu/UuX6XMPEogDAxkuUFeZn60pXNHAqn3A==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" + }, "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", @@ -3915,6 +4117,57 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/prebuild-install": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", + "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -3949,6 +4202,15 @@ "react-is": "^16.13.1" } }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -3977,6 +4239,33 @@ } ] }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", @@ -4048,6 +4337,19 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4198,6 +4500,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -4224,7 +4545,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -4240,6 +4560,28 @@ "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==" }, + "node_modules/sharp": { + "version": "0.32.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.5.tgz", + "integrity": "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -4275,6 +4617,62 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -4308,6 +4706,23 @@ "node": ">=10.0.0" } }, + "node_modules/streamx": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.1.tgz", + "integrity": "sha512-fQMzy2O/Q47rgwErk/eGeLu/roaFWV0jVsogDmrszM9uIw8L5OA+t+V93MgYlufNptfjmYR1tOMWhei/Eh7TQA==", + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz", @@ -4553,6 +4968,26 @@ "node": ">=6" } }, + "node_modules/tar-fs": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", + "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "dependencies": { + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + } + }, + "node_modules/tar-stream": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", + "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -4631,6 +5066,17 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4923,8 +5369,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yaml": { "version": "2.3.1", diff --git a/Website/package.json b/Website/package.json index a231edf6..4e8c44cd 100644 --- a/Website/package.json +++ b/Website/package.json @@ -25,6 +25,7 @@ "react-dom": "18.2.0", "react-select": "^5.7.4", "server-only": "^0.0.1", + "sharp": "^0.32.5", "swr": "^2.2.0", "tailwindcss": "3.3.2" }, diff --git a/Website/public/generic_rail_bound_vehicle.svg b/Website/public/generic_rail_bound_vehicle.svg deleted file mode 100644 index 17eff589..00000000 --- a/Website/public/generic_rail_bound_vehicle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Website/public/poiTypeIcons/generic.svg b/Website/public/poiTypeIcons/generic.svg new file mode 100644 index 00000000..df59a54e --- /dev/null +++ b/Website/public/poiTypeIcons/generic.svg @@ -0,0 +1,15 @@ + + + + \ No newline at end of file diff --git a/Website/public/poiTypeIcons/generic_rail_bound_vehicle.svg b/Website/public/poiTypeIcons/generic_rail_bound_vehicle.svg deleted file mode 100644 index 77dbbae4..00000000 --- a/Website/public/poiTypeIcons/generic_rail_bound_vehicle.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Website/public/poiTypeIcons/lesser_level_crossing.svg b/Website/public/poiTypeIcons/lesser_level_crossing.svg index 72fa7632..7668b1b5 100644 --- a/Website/public/poiTypeIcons/lesser_level_crossing.svg +++ b/Website/public/poiTypeIcons/lesser_level_crossing.svg @@ -1,3 +1,15 @@ - - + + diff --git a/Website/public/poiTypeIcons/level_crossing.svg b/Website/public/poiTypeIcons/level_crossing.svg index a8fef462..6df9ce81 100644 --- a/Website/public/poiTypeIcons/level_crossing.svg +++ b/Website/public/poiTypeIcons/level_crossing.svg @@ -1,4 +1,18 @@ - - - + + + diff --git a/Website/public/poiTypeIcons/parking.svg b/Website/public/poiTypeIcons/parking.svg deleted file mode 100644 index 92372c0f..00000000 --- a/Website/public/poiTypeIcons/parking.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Website/public/poiTypeIcons/passing_position.svg b/Website/public/poiTypeIcons/passing_position.svg new file mode 100644 index 00000000..fd8f8bde --- /dev/null +++ b/Website/public/poiTypeIcons/passing_position.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + diff --git a/Website/public/poiTypeIcons/picnic.svg b/Website/public/poiTypeIcons/picnic.svg new file mode 100644 index 00000000..fa805ccf --- /dev/null +++ b/Website/public/poiTypeIcons/picnic.svg @@ -0,0 +1,20 @@ + + + + + + diff --git a/Website/public/poiTypeIcons/track_end.svg b/Website/public/poiTypeIcons/track_end.svg new file mode 100644 index 00000000..a1cbd278 --- /dev/null +++ b/Website/public/poiTypeIcons/track_end.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + diff --git a/Website/public/poiTypeIcons/turning_point.svg b/Website/public/poiTypeIcons/turning_point.svg new file mode 100644 index 00000000..b7c6a228 --- /dev/null +++ b/Website/public/poiTypeIcons/turning_point.svg @@ -0,0 +1,22 @@ + + + + + diff --git a/Website/public/vehicle/Vehicle_background_neutral.svg b/Website/public/vehicle/Vehicle_background_neutral.svg index 69b69302..8df92c0c 100644 --- a/Website/public/vehicle/Vehicle_background_neutral.svg +++ b/Website/public/vehicle/Vehicle_background_neutral.svg @@ -1,3 +1,3 @@ - + diff --git a/Website/public/vehicle/Vehicle_background_heading.svg b/Website/public/vehicle/Vehicle_heading.svg similarity index 77% rename from Website/public/vehicle/Vehicle_background_heading.svg rename to Website/public/vehicle/Vehicle_heading.svg index a0e7b9bd..390883fc 100644 --- a/Website/public/vehicle/Vehicle_background_heading.svg +++ b/Website/public/vehicle/Vehicle_heading.svg @@ -1,4 +1,3 @@ - diff --git a/Website/src/app/components/dynlist.tsx b/Website/src/app/components/dynlist.tsx index c903f5f6..2e084af3 100644 --- a/Website/src/app/components/dynlist.tsx +++ b/Website/src/app/components/dynlist.tsx @@ -53,25 +53,25 @@ export default function DynamicList({ server_vehicles, track_id, logged_in, trac - - - - - - + + + + + + {sorted_vehicles?.map(v => ( - - - diff --git a/Website/src/app/components/iconSelection.tsx b/Website/src/app/components/iconSelection.tsx index 55c32cfc..d21514ee 100644 --- a/Website/src/app/components/iconSelection.tsx +++ b/Website/src/app/components/iconSelection.tsx @@ -1,13 +1,10 @@ import Select, { Options, SingleValue } from "react-select"; import { Option } from "@/utils/types"; import { useMemo } from "react"; +import { POIIconCommonName, POIIconImg } from "@/utils/common"; +import { POITypeIcon, POITypeIconValues } from "@/utils/api"; -export const icons = [ - { path: "/poiTypeIcons/generic_rail_bound_vehicle.svg", name: "Schienenfahrzeug" }, - { path: "/poiTypeIcons/level_crossing.svg", name: "Bahnübergang" }, - { path: "/poiTypeIcons/lesser_level_crossing.svg", name: "Unbeschilderter Bahnübergang" }, - { path: "/poiTypeIcons/parking.svg", name: "Haltepunkt" } -]; +const POI_ICONS: POITypeIcon[] = Object.values(POITypeIconValues); /** * A consolidated icon selection component @@ -20,27 +17,29 @@ export default function IconSelection({ id, name }: { - currentIcon: string; - setIcon: (newIcon: string) => void; + currentIcon: POITypeIcon | ""; + setIcon: (newIcon: POITypeIcon | "") => void; setModified?: (modified: boolean) => void; className?: string; id: string; name: string; }) { - const iconOptions: Options> = useMemo( + const iconOptions: Options> = useMemo( () => - icons.map(i => ({ - value: i.path, + POI_ICONS.map(i => ({ + value: i, label: ( -
- {i.name} -
{i.name}
+
+
+ {POIIconCommonName[i]} +
+
{POIIconCommonName[i]}
) })), [] ); - const defaultIcon: Option = useMemo( + const defaultIcon: Option<""> = useMemo( () => ({ value: "", label: ( @@ -52,14 +51,14 @@ export default function IconSelection({ [] ); - const icon = useMemo( + const icon: Option = useMemo( () => iconOptions.find(v => v.value === currentIcon) ?? defaultIcon, [currentIcon, iconOptions, defaultIcon] ); console.log("Icon for", currentIcon, icon); - function changeFunction(newValue: SingleValue>) { - if (newValue) { + function changeFunction(newValue: SingleValue>) { + if (newValue && newValue.value !== "") { setIcon(newValue.value); setModified ? setModified(true) : undefined; } diff --git a/Website/src/app/components/login_wrap.tsx b/Website/src/app/components/login_wrap.tsx index c307fbcb..0ee1fa64 100644 --- a/Website/src/app/components/login_wrap.tsx +++ b/Website/src/app/components/login_wrap.tsx @@ -1,8 +1,8 @@ -"use client" -import {IMapRefreshConfig} from "@/utils/types"; -import {useState} from "react"; -import {LoginDialog} from "@/app/components/login"; -import {SelectionDialog} from "@/app/components/track_selection"; +"use client"; +import { IMapRefreshConfig } from "@/utils/types"; +import { useState } from "react"; +import { LoginDialog } from "@/app/components/login"; +import { SelectionDialog } from "@/app/components/track_selection"; /** * Component wrapping some other component with a login- and track selection dialog and keeping track of login state. @@ -11,21 +11,37 @@ import {SelectionDialog} from "@/app/components/track_selection"; * @param map_conf parameters for the construction of the child * @param child Function contructing the wrapped React Component. */ -const LoginWrapper = ({logged_in, track_selected, map_conf, child}: {logged_in: boolean, track_selected: boolean, map_conf: IMapRefreshConfig, child: (conf: IMapRefreshConfig) => JSX.Element}) => { - const [loginState, setLogin] = useState(logged_in); +const LoginWrapper = ({ + logged_in, + track_selected, + map_conf, + child +}: { + logged_in: boolean; + track_selected: boolean; + map_conf: IMapRefreshConfig; + child: (conf: IMapRefreshConfig) => JSX.Element; +}) => { + const [loginState, setLogin] = useState(logged_in); - // console.log('track selected', track_selected, map_conf.track_id) + // console.log('track selected', track_selected, map_conf.track_id) - return <> - {!loginState && - -

You need to log in!

-
} - {loginState && !track_selected && -

Please select a track!

-
} - {child({...map_conf, logged_in: loginState, setLogin: setLogin})} - -} + return ( + <> + {!loginState ? ( + +

Sie müssen sich einloggen!

+
+ ) : ( + !track_selected && ( + +

Bitte wählen Sie eine Strecke aus

+
+ ) + )} + {child({ ...map_conf, logged_in: loginState, setLogin: setLogin })} + + ); +}; -export default LoginWrapper; \ No newline at end of file +export default LoginWrapper; diff --git a/Website/src/app/components/map.tsx b/Website/src/app/components/map.tsx index 89a94c14..d044ac66 100644 --- a/Website/src/app/components/map.tsx +++ b/Website/src/app/components/map.tsx @@ -8,7 +8,8 @@ import { coordinateFormatter } from "@/utils/helpers"; import assert from "assert"; import { createPortal } from "react-dom"; import RotatingVehicleIcon from "@/utils/rotatingIcon"; -import { PointOfInterest, POIType } from "@/utils/api"; +import { PointOfInterest, POIType, POITypeIconValues } from "@/utils/api"; +import { POIIconImg } from "@/utils/common"; function poiPopupFactory(poi: PointOfInterest, poi_type?: POIType): HTMLDivElement { const container = document.createElement("div"); @@ -48,10 +49,19 @@ function Map({ // find the vehicle that is in focus, but only if either the vehicles, or the focus changes. const vehicleInFocus = useMemo(() => vehicles.find(v => v.id == focus), [vehicles, focus]); - // create icons for each poi type - const enriched_poi_types: (POIType & {leaf_icon: L.Icon})[] = useMemo( - () => poi_types.map(pt => ({ ...pt, leaf_icon: L.icon({ iconUrl: pt.icon, iconSize: [45, 45] }) })), + const enriched_poi_types: (POIType & { leaf_icon: L.Icon })[] = useMemo( + () => + poi_types.map(pt => { + const icon_src = POIIconImg[pt.icon] ?? POIIconImg[POITypeIconValues.Generic]; + console.log("poi_icon for", pt.name, pt.icon, "at", icon_src); + const leaf_icon = L.icon({ iconUrl: icon_src, iconSize: [45, 45] }); + + return { + ...pt, + leaf_icon + }; + }), [poi_types] ); diff --git a/Website/src/app/list/page.tsx b/Website/src/app/list/page.tsx index 2fe61278..fb2d82c7 100644 --- a/Website/src/app/list/page.tsx +++ b/Website/src/app/list/page.tsx @@ -28,7 +28,7 @@ export default async function Home() { console.log("server vehicles", server_vehicles); return (
-
+
); // TODO: handle fetching errors - assert(!err); + assert(true || !err); const initialPos = L.latLng({ lat: 54.2333, lng: 10.6024 }); @@ -44,7 +44,6 @@ export default function POIManagement({ poiTypes, tracks }: { poiTypes: POIType[ const [poiType, setPoiType] = useState(""); const [poiDescription, setPoiDescription] = useState(""); const [poiPosition, setPoiPosition] = useState(initialPos); - const [poiIsTurningPoint, setPoiIsTurningPoint] = useState(false); /** modified: A "dirty flag" to prevent loosing information. */ const [modified, setModified] = useState(false); @@ -69,7 +68,7 @@ export default function POIManagement({ poiTypes, tracks }: { poiTypes: POIType[ const updatePayload: UpdatePointOfInterest = { id: selPoi.value === "" ? undefined : selPoi.value, - isTurningPoint: poiIsTurningPoint, + isTurningPoint: false, pos: apiPos, trackId: +poiTrack, name: poiName, @@ -161,7 +160,6 @@ export default function POIManagement({ poiTypes, tracks }: { poiTypes: POIType[ setPoiTrack("" + (selectedPOI?.trackId ?? "")); setPoiType("" + (selectedPOI?.typeId ?? "")); setPoiDescription(selectedPOI?.description ?? ""); - setPoiIsTurningPoint(selectedPOI?.isTurningPoint ?? false); setPoiPosition(selectedPOI?.pos ? L.latLng(selectedPOI?.pos) : initialPos); // Also reset the "dirty flag" setModified(false); @@ -186,14 +184,32 @@ export default function POIManagement({ poiTypes, tracks }: { poiTypes: POIType[ name={"selPoi"} className="col-span-5 border border-gray-500 dark:bg-slate-700 rounded" options={poiOptions} + unstyled={true} classNames={ /* The zoom controls of the leaflet map use a z-index of 1000. So to display the select dropdown in front of the map, we need the z-index to be > 1000. - Unfortionately, react-select sets the z-index to 1, without an obvious way + Unfortunately, react-select sets the z-index to 1, without an obvious way to change this, so we use an important class. + The same applies to background color, which is why we need to set that one + important for proper dark-mode support... */ - { menu: () => "!z-1100" } + { + menu: () => "!z-1100 dark:bg-slate-700 bg-white my-2 rounded-md drop-shadow-lg", + valueContainer: () => "mx-3", + dropdownIndicator: () => "m-2 text-gray-500 transition-colors hover:dark:text-gray-50 hover:text-gray-950", + indicatorSeparator: () => "bg-gray-200 dark:bg-gray-500 my-2", + menuList: () => "py-1", + option: (state) => { + if (state.isSelected) { + return "px-3 py-2 dark:bg-blue-200 dark:text-black bg-blue-800 text-white"; + } else if (state.isFocused) { + return "px-3 py-2 bg-blue-100 dark:bg-blue-900"; + } else { + return "px-3 py-2"; + } + } + } } />
Namegeog. Breitegeog. LängeRichtungBatterieladungAuf Karte anzeigenNameBatterieladungAuf Karte anzeigen
{v.name} + + + {{}.toString()}