diff --git a/Server/src/routes/init.route.ts b/Server/src/routes/init.route.ts index b7344c29..651532c1 100644 --- a/Server/src/routes/init.route.ts +++ b/Server/src/routes/init.route.ts @@ -63,25 +63,14 @@ export class InitRoute { const track: Track = await database.tracks.getById(id) - const lineString: Feature | null = TrackService.getTrackAsLineString(track) - if (!lineString) { - logger.error(`Could not convert track to line string`) - res.sendStatus(500) - return - } + const lineString: Feature = TrackService.getTrackAsLineString(track) const path: FeatureCollection = { type: "FeatureCollection", features: [lineString] } - const length: number | null = TrackService.getTrackLength(track) - if (length == null) { - logger.error(`Could not determine length of track with id ${id}`) - res.sendStatus(500) - return - } - + const length: number = TrackService.getTrackLength(track) const pois: POI[] = await POIService.getAllPOIsForTrack(track) const apiPois: z.infer[] = await this.getAppPoisFromDbPoi(pois) @@ -126,31 +115,13 @@ export class InitRoute { geometry: { type: "Point", coordinates: [pos.lng, pos.lat] }, properties: null } - const currentTrack: Track | null = await TrackService.getClosestTrack(backendPos) - - if (!currentTrack) { - logger.error(`Could not find current track with position {lat : ${pos.lat}, lng : ${pos.lng}}`) - res.sendStatus(500) - return - } - - const length: number | null = TrackService.getTrackLength(currentTrack) - - if (length == null) { - logger.error(`Length of track with id ${currentTrack.uid} could not be determined`) - res.sendStatus(500) - return - } + const currentTrack: Track = await TrackService.getClosestTrack(backendPos) + const length: number = TrackService.getTrackLength(currentTrack) const pois: POI[] = await POIService.getAllPOIsForTrack(currentTrack) const apiPois: z.infer[] = await this.getAppPoisFromDbPoi(pois) - const lineString: Feature | null = TrackService.getTrackAsLineString(currentTrack) - if (!lineString) { - logger.error(`Could not read track with id ${currentTrack.uid} as line string`) - res.sendStatus(500) - return - } + const lineString: Feature = TrackService.getTrackAsLineString(currentTrack) const path: FeatureCollection = { type: "FeatureCollection", @@ -195,18 +166,24 @@ export class InitRoute { // ensure that the app always gets an enum member. const appType: z.infer = poiIcon in POITypeIcon.enum ? poiIcon : POITypeIconEnum.Generic - const geoJsonPos: Feature | null = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) - if (!geoJsonPos) { - logger.error(`Could not find position of POI with id ${poi.uid}`) + let geoJsonPos: Feature + try { + geoJsonPos = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) + } catch (error) { + logger.warn(`Could not find position of POI with id ${poi.uid}`) continue } + const pos: z.infer = { lat: GeoJSONUtils.getLatitude(geoJsonPos), lng: GeoJSONUtils.getLongitude(geoJsonPos) } - const percentagePosition: number | null = await POIService.getPOITrackDistancePercentage(poi) - if (percentagePosition == null) { - logger.error(`Could not determine percentage position of poi with id ${poi.uid}`) + + let percentagePosition: number + try { + percentagePosition = await POIService.getPOITrackDistancePercentage(poi) + } catch (err) { + logger.warn(`Could not determine percentage position of poi with id ${poi.uid}`) continue } diff --git a/Server/src/routes/poi.route.ts b/Server/src/routes/poi.route.ts index e46fa8c0..da4ff6ab 100644 --- a/Server/src/routes/poi.route.ts +++ b/Server/src/routes/poi.route.ts @@ -45,13 +45,9 @@ export class PoiRoute { private async getAllPOIs(_req: Request, res: Response): Promise { const pois: POI[] = await database.pois.getAll() - const typedPOIs: (z.infer | null)[] = pois.map( + const typedPOIs: z.infer[] = pois.map( ({ uid, name, trackId, description, isTurningPoint, typeId, position }) => { - const geoJsonPos: Feature | null = GeoJSONUtils.parseGeoJSONFeaturePoint(position) - if (!geoJsonPos) { - logger.error(`Could not find position of POI with id ${uid}`) - return null - } + const geoJsonPos: Feature = GeoJSONUtils.parseGeoJSONFeaturePoint(position) const pos: z.infer = { lat: GeoJSONUtils.getLatitude(geoJsonPos), lng: GeoJSONUtils.getLongitude(geoJsonPos) @@ -80,12 +76,7 @@ export class PoiRoute { const poi: POI = await database.pois.getById(poiId) - const geoPos: Feature | null = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) - if (!geoPos) { - logger.error(`Could not find position of POI with id ${poi.uid}`) - res.sendStatus(500) - return - } + const geoPos: Feature = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) const pos: z.infer = { lat: GeoJSONUtils.getLatitude(geoPos), lng: GeoJSONUtils.getLongitude(geoPos) @@ -126,7 +117,7 @@ export class PoiRoute { const type: POIType = await database.pois.getTypeById(poiPayload.typeId) - const newPOI: POI | null = await POIService.createPOI( + const newPOI: POI = await POIService.createPOI( geopos, poiPayload.name ? poiPayload.name : "", type, @@ -135,12 +126,6 @@ export class PoiRoute { poiPayload.isTurningPoint ) - if (!newPOI) { - logger.error(`Could not create new POI`) - res.sendStatus(500) - return - } - res.json({ id: newPOI.uid }) return } diff --git a/Server/src/routes/track.route.ts b/Server/src/routes/track.route.ts index 4a8f9a5c..96290f6c 100644 --- a/Server/src/routes/track.route.ts +++ b/Server/src/routes/track.route.ts @@ -5,7 +5,7 @@ import { POI, Track, Vehicle } from "@prisma/client" import please_dont_crash from "../utils/please_dont_crash" import { logger } from "../utils/logger" import { BareTrack, FullTrack, PointOfInterest, Position, UpdateTrack, Vehicle as APIVehicle } from "../models/api" -import VehicleService from "../services/vehicle.service" +import VehicleService, { VehicleData } from "../services/vehicle.service" import { Feature, LineString, Point } from "geojson" import POIService from "../services/poi.service" import GeoJSONUtils from "../utils/geojsonUtils" @@ -97,20 +97,8 @@ export class TrackRoute { const track: Track = await database.tracks.getById(trackId) // derive and transform the database data for easier digestion by the clients. - const path: Feature | null = TrackService.getTrackAsLineString(track) - const length: number | null = TrackService.getTrackLength(track) - - if (!path) { - logger.error(`Could not get track with id ${track.uid} as a line string`) - res.sendStatus(500) - return - } - - if (length == null) { - logger.error(`Length of track with id ${track.uid} could not be determined`) - res.sendStatus(500) - return - } + const path: Feature = TrackService.getTrackAsLineString(track) + const length: number = TrackService.getTrackLength(track) // Build the response object const api_track: z.infer = { @@ -197,9 +185,16 @@ export class TrackRoute { // obtain vehicles associated with the track from the db. const vehicles: Vehicle[] = await database.vehicles.getAll(track.uid) const ret: z.infer[] = await Promise.allSettled( - vehicles.map(async (vehicle: Vehicle) => { + vehicles.flatMap(async (vehicle: Vehicle) => { // get the current data of the vehicle - const vehicleData = await VehicleService.getVehicleData(vehicle) + let vehicleData: VehicleData + try { + vehicleData = await VehicleService.getVehicleData(vehicle) + } catch (err) { + logger.warn(`Could not compute vehicle data for vehicle ${vehicle.uid}.`) + return [] + } + // If we know that, convert it in the API format. const pos: z.infer | undefined = { lat: GeoJSONUtils.getLatitude(vehicleData.position), @@ -242,22 +237,24 @@ export class TrackRoute { const pois: POI[] = await database.pois.getAll(trackId) const ret: z.infer[] = ( await Promise.all( - pois.map(async (poi: POI) => { - const pos: Feature | null = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) - if (!pos) { - logger.error(`Could not find position of POI with id ${poi.uid}`) - // res.sendStatus(500) + pois.flatMap(async (poi: POI) => { + let pos: Feature + try { + pos = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) + } catch (err) { + logger.warn(`Could not find position of POI with id ${poi.uid}`) return [] } const actualPos: z.infer = { lat: GeoJSONUtils.getLatitude(pos), lng: GeoJSONUtils.getLongitude(pos) } - const percentagePosition: number | null = await POIService.getPOITrackDistancePercentage(poi) - if (percentagePosition == null) { - logger.error(`Could not find percentage position of POI with id ${poi.uid}`) - // res.sendStatus(500) + let percentagePosition: number + try { + percentagePosition = await POIService.getPOITrackDistancePercentage(poi) + } catch (err) { + logger.warn(`Could not find percentage position of POI with id ${poi.uid}`) return [] } diff --git a/Server/src/routes/tracker.route.ts b/Server/src/routes/tracker.route.ts index 72830bdc..b2fdec99 100644 --- a/Server/src/routes/tracker.route.ts +++ b/Server/src/routes/tracker.route.ts @@ -152,7 +152,7 @@ export class TrackerRoute { res.sendStatus(200) return } - const timestamp = new Date() + const timestamp = new Date(trackerDataPayload.received_at) const longitude = trackerDataPayload.uplink_message.decoded_payload.longitudeDeg const latitude = trackerDataPayload.uplink_message?.decoded_payload?.latitudeDeg const heading = trackerDataPayload.uplink_message.decoded_payload.headingDeg @@ -198,6 +198,7 @@ export class TrackerRoute { let latitude = 0.0 let heading = 0 let speed = 0 + let timestamp = new Date() let field0Present = false let battery = undefined @@ -213,6 +214,7 @@ export class TrackerRoute { latitude = gpsField.Lat heading = gpsField.Head speed = gpsField.Spd + timestamp = new Date(gpsField.GpsUTC.replace(" ", "T").concat("Z")) break } case 6: { @@ -231,7 +233,7 @@ export class TrackerRoute { } await TrackerService.appendLog( associatedVehicle, - new Date(), // TODO: use payload timestamp + timestamp, [longitude, latitude], heading, speed, diff --git a/Server/src/services/poi.service.ts b/Server/src/services/poi.service.ts index 38078215..7ddca5e0 100644 --- a/Server/src/services/poi.service.ts +++ b/Server/src/services/poi.service.ts @@ -16,7 +16,11 @@ export default class POIService { * @param track `Track` the new POI belongs to, if no track is given, the closest will be chosen * @param description optional description of the new POI * @param isTurningPoint is the new POI a point, where one can turn around their vehicle (optional) - * @returns created `POI` if successful, `null` otherwise + * @returns created `POI` if successful + * @throws `HTTPError` + * - if the closest track could not be computed (if none is given) + * - if the position could not be enriched + * @throws PrismaError, if saving the created POI to database was not possible */ public static async createPOI( position: GeoJSON.Feature, @@ -25,24 +29,15 @@ export default class POIService { track?: Track, description?: string, isTurningPoint?: boolean - ): Promise { + ): Promise { // TODO: check if poi is anywhere near the track // get closest track if none is given 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 + track = await TrackService.getClosestTrack(position) } // 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 - } // Note: geopos is from type GeoJSON.Feature and can't be parsed directly into Prisma.InputJsonValue // Therefore we cast it into unknown first. @@ -60,28 +55,22 @@ export default class POIService { * Add value of track kilometer to properties for a given point * @param point position of POI to enrich * @param track optional `TracK`, which is used to compute the track kilometer, if none is given the closest will be used - * @returns point with added track kilometer, `null` if not successful + * @returns point with added track kilometer + * @throws `HTTPError` + * - if the track kilometer value could not be computed + * - if the closest track could not be computed (if none is given) */ public static async enrichPOIPosition( point: GeoJSON.Feature, track?: Track - ): Promise | null> { + ): Promise> { // initialize track if none is given if (track == null) { - const tempTrack = await TrackService.getClosestTrack(point) - if (tempTrack == null) { - logger.error(`No closest track was found for position ${JSON.stringify(point)}.`) - return null - } - track = tempTrack + track = await TrackService.getClosestTrack(point) } // calculate and set track kilometer - const trackKm = await TrackService.getPointTrackKm(point, track) - if (trackKm == null) { - logger.error(`Could not get track distance for position ${JSON.stringify(point)} on track with id ${track.uid}.`) - return null - } + const trackKm = TrackService.getPointTrackKm(point, track) GeoJSONUtils.setTrackKm(point, trackKm) return point } @@ -89,48 +78,39 @@ export default class POIService { /** * Wrapper to get distance of poi in kilometers along the assigned track * @param poi `POI` to get the distance for - * @returns track kilometer of `poi`, `null` if computation was not possible + * @returns track kilometer of `poi` + * @throws `HTTPError` + * - if the track kilometer value of `poi` could not be accessed after trying to enrich it + * - if the position of `poi` could not be parsed + * - if the position of `poi` could not be enriched + * @throws PrismaError + * - if accessing the track of `poi` from the database was not possible + * - if updating `poi` in the database was not possible */ - public static async getPOITrackDistanceKm(poi: POI): Promise { + public static async getPOITrackDistanceKm(poi: POI): Promise { // get closest track if none is given const poiPos = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) - if (poiPos == null) { - logger.error(`Position ${JSON.stringify(poi.position)} could not be parsed.`) - return null - } // get track distance in kilometers - let poiTrackKm = GeoJSONUtils.getTrackKm(poiPos) - if (poiTrackKm == null) { - if (poiTrackKm == null) { - logger.info(`Position of POI with ID ${poi.uid} is not enriched yet.`) - // the poi position is not "enriched" yet. - // 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) - - // 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}.`) - return null - } - // try to update the poi in the database, now that we have enriched it - await database.pois.update(poi.uid, { position: enrichedPos as unknown as Prisma.InputJsonValue }) - - // 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 track kilometer of POI position ${JSON.stringify(enrichedPos)}.`) - return null - } - } + let poiTrackKm + try { + poiTrackKm = GeoJSONUtils.getTrackKm(poiPos) + } catch (err) { + logger.info(`Position of POI with ID ${poi.uid} is not enriched yet.`) + // the poi position is not "enriched" yet. + // Therefore, obtain and typecast the position + const poiPos = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) + + // get track of POI to enrich it + const track = await database.tracks.getById(poi.trackId) + + // then enrich it with the given track + const enrichedPos = await this.enrichPOIPosition(poiPos, track) + // try to update the poi in the database, now that we have enriched it + await database.pois.update(poi.uid, { position: enrichedPos as unknown as Prisma.InputJsonValue }) + + // and re-calculate poiTrackKm (we do not care that much at this point if the update was successful) + poiTrackKm = GeoJSONUtils.getTrackKm(enrichedPos) } return poiTrackKm } @@ -138,25 +118,19 @@ export default class POIService { /** * Compute distance of given POI as percentage along the assigned track * @param poi `POI` to compute distance for - * @returns percentage of track distance of `poi`, `null` if computation was not possible + * @returns percentage of track distance of `poi` + * @throws `HTTPError` + * - if the track length could not be computed + * - if the track kilometer of `poi` could not be computed + * @throws PrismaError, if the track of `poi` could not be accessed in the database */ - public static async getPOITrackDistancePercentage(poi: POI): Promise { + public static async getPOITrackDistancePercentage(poi: POI): Promise { // get track length const track = await database.tracks.getById(poi.trackId) - 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 track kilometer of POI with ID ${poi.uid}.`) - return null - } // compute percentage + const poiDistKm = await this.getPOITrackDistanceKm(poi) return (poiDistKm / trackLength) * 100 } diff --git a/Server/src/services/track.service.ts b/Server/src/services/track.service.ts index 61a59ceb..a29eee60 100644 --- a/Server/src/services/track.service.ts +++ b/Server/src/services/track.service.ts @@ -8,6 +8,7 @@ import * as turfMeta from "@turf/meta" import * as turfHelpers from "@turf/helpers" import bearing from "@turf/bearing" import { logger } from "../utils/logger" +import { HTTPError } from "../models/error" /** * Service for track management. This also includes handling the GeoJSON track data. @@ -18,7 +19,9 @@ export default class TrackService { * @param track `GeoJSON.FeatureCollection` of points of track, this has to be ordered * @param start starting location of the track * @param dest destination of track (currently in modelling start and end point do not differentiate) - * @returns `Track` if creation was successful, `null` otherwise + * @returns `Track` if creation was successful + * @throws `HTTPError`, if enriching the track data was not possible + * @throws PrismaError, if saving the track to database was not possible */ public static createTrack( track: GeoJSON.FeatureCollection, @@ -38,7 +41,9 @@ export default class TrackService { * @param path `GeoJSON.FeatureCollection` of points of track, this has to be ordered * @param start starting location of the track * @param dest destination of track (currently in modelling start and end point do not differentiate) - * @returns `Track` if creation was successful, `null` otherwise + * @returns `Track` if creation was successful + * @throws `HTTPError`, if enriching the track data was not possible + * @throws PrismaError, if updating the track in the database was not possible */ public static updateTrack( track_uid: number, @@ -61,6 +66,7 @@ export default class TrackService { * Assign each point of given track data its track kilometer * @param track `GeoJSON.FeatureCollection` of points of track to process * @returns enriched data of track + * @throws `HTTPError`, if a track kilometer value of a feature from track data could not be accessed */ private static enrichTrackData( track: GeoJSON.FeatureCollection @@ -87,15 +93,14 @@ export default class TrackService { * Calculate projected track kilometer for a given position * @param position position to calculate track kilometer for (does not need to be on the track) * @param track `Track` to use for calculation - * @returns track kilometer of `position` projected on `track`, `null` if an error occurs + * @returns track kilometer of `position` projected on `track` + * @throws `HTTPError` + * - if `position` could not be projected onto the track + * - if the track kilometer value could not be accessed from the projected point */ - public static getPointTrackKm(position: GeoJSON.Feature, track: Track): number | null { + public static getPointTrackKm(position: GeoJSON.Feature, track: Track): number { // get the track kilometer value from projected point const projectedPoint = this.getProjectedPointOnTrack(position, track) - if (projectedPoint == null) { - logger.error(`Could not project position ${JSON.stringify(position)}.`) - return null - } return GeoJSONUtils.getTrackKm(projectedPoint) } @@ -103,20 +108,18 @@ export default class TrackService { * Calculate percentage value for given track kilometer of given track * @param trackKm track kilometer value to convert to percentage * @param track `Track` to use for calculation as reference - * @returns percentage value of `trackKm` regarding `track`, `null` if an error occurs + * @returns percentage value of `trackKm` regarding `track` + * @throws `HTTPError` + * - if the track length could not be computed + * - if `trackKm` is not between 0 and track length */ - public static getTrackKmAsPercentage(trackKm: number, track: Track): number | null { + public static getTrackKmAsPercentage(trackKm: number, track: Track): number { // get total track length in kilometers const trackLength = this.getTrackLength(track) - if (trackLength == null) { - logger.error(`Could not compute track length from track with id ${track.uid} to convert track kilometer value.`) - return null - } // check if track kilometer is within bounds if (trackKm < 0 || trackKm > trackLength) { - logger.error(`Expected track kilometer to be between 0 and ${trackLength}, but got ${trackKm}.`) - return null + throw new HTTPError(`Expected track kilometer to be between 0 and ${trackLength}, but got ${trackKm}.`, 500) } // convert to percentage @@ -127,18 +130,17 @@ export default class TrackService { * Project a position onto a track * @param position position to project onto the track * @param track `Track` to project `position` onto - * @returns track point, which is the `position` projected onto `track`, enriched with a track kilometer value, `null` if an error occurs + * @returns track point, which is the `position` projected onto `track`, enriched with a track kilometer value + * @throws `HTTPError` + * - if turf did not set "location"-property while computing nearest-point-on-line + * - if the track data could not be parsed */ public static getProjectedPointOnTrack( position: GeoJSON.Feature, track: Track - ): GeoJSON.Feature | null { + ): GeoJSON.Feature { // converting feature collection of points from track to linestring to project position onto it const trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(track.data) - if (trackData == null) { - 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 @@ -147,13 +149,13 @@ export default class TrackService { // for easier access we set the property of track kilometer to the already calculated value if (projectedPoint.properties["location"] == null) { - logger.error( + // this is a slight overreaction as we can still return the projected point, but the track kilometer property will not be accessible + throw new HTTPError( `Turf error: Could not calculate nearest point on line correctly for position ${JSON.stringify( position - )} and for linestring of track with id ${track.uid}.` + )} and for linestring of track with id ${track.uid}.`, + 500 ) - // this is a slight overreaction as we can still return the projected point, but the track kilometer property will not be accessible - return null } GeoJSONUtils.setTrackKm(projectedPoint, projectedPoint.properties["location"]) return projectedPoint @@ -163,31 +165,30 @@ export default class TrackService { * Calculate current heading of track for a given distance / track kilometer * @param track `Track` to get heading for * @param trackKm distance of `track` to get heading for - * @returns current heading (0-359) of `track` at distance `trackKm`, `null` if an error occurs + * @returns current heading (0-359) of `track` at distance `trackKm` + * @throws getTrackKm + * @throws `HTTPError` + * - if the track length could not be computed + * - if `trackKm` is not between 0 and track length + * - if track kilometer value was not found while iterating through `track` + * - if the track kilometer value of a feature of the track could not be accessed + * - if the track data could not be parsed */ - public static getTrackHeading(track: Track, trackKm: number): number | null { + public static getTrackHeading(track: Track, trackKm: number): number { // TODO quite inefficient? did not found anything from turf, that could do this in a simple way // TODO: maybe enrich track with bearing as well // validate track kilometer value const trackLength = this.getTrackLength(track) - if (trackLength == null) { - 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}.` + throw new HTTPError( + `Unexpected value for track kilometer: ${trackKm}. This needs to be more than 0 and less than ${trackLength}.`, + 500 ) - return null } // get track data const trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(track.data) - if (trackData == null) { - logger.error(`Could not parse track data of track with id ${track.uid}.`) - return null - } // check if we are right at the beginning of the track (not covered by loop below) if (trackKm == 0) { @@ -198,44 +199,40 @@ export default class TrackService { for (let i = 1; i < trackData.features.length; i++) { const trackPoint = trackData.features[i] const trackPointKm = GeoJSONUtils.getTrackKm(trackPoint) - if (trackPointKm == null) { - logger.error(`Could not access track kilometer value of track point ${i} of track with id ${track.uid}.`) - return null - } - - // actual check if (trackKm <= trackPointKm) { return bearing(trackData.features[i - 1], trackPoint) } } - logger.error( - `Track kilometer value ${trackKm} could not be found while iterating track points of track with id ${track.uid}.` + throw new HTTPError( + `Track kilometer value ${trackKm} could not be found while iterating track points of track with id ${track.uid}.`, + 500 ) - return null } /** * Look for the closest track for a given position * @param position position to search the closest track for - * @returns closest `Track` for `position` or `null` if an error occurs + * @returns closest `Track` for `position` + * @throws `HTTPError`, if no closest track was found or no track exists */ - public static async getClosestTrack(position: GeoJSON.Feature): Promise { + public static async getClosestTrack(position: GeoJSON.Feature): Promise { const tracks = await database.tracks.getAll() // there are no tracks at all if (tracks.length == 0) { - logger.warn(`No track was found.`) - return null + throw new HTTPError(`No track was found.`, 404) } // find closest track by iterating let minDistance = Number.POSITIVE_INFINITY let minTrack = -1 for (let i = 0; i < tracks.length; i++) { - const trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(tracks[i].data) - if (trackData == null) { - logger.error(`Could not parse track data of track with id ${tracks[i].uid}.`) - return null + let trackData + try { + trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(tracks[i].data) + } catch (err) { + logger.warn(`Could not parse data of track ${tracks[i].uid}`) + continue } // converting feature collection of points to linestring to measure distance @@ -260,8 +257,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 + throw new HTTPError(`Somehow no closest track was found even after iterating all existing tracks.`, 500) } else { return tracks[minTrack] } @@ -271,37 +267,29 @@ export default class TrackService { * Get total distance for a given track. This is just for easier access as the track kilometer * of the last track point is essentially the length of the track. * @param track `Track` to get the length of - * @returns lenth of `track` in kilometers if possible, `null` otherwise (this could be caused by invalid track data) + * @returns lenth of `track` in kilometers if possible + * @throws `HTTPError` + * - if the track kilometer value of the last feature of `track` could not be accessed + * - if the track data could not be parsed */ - public static getTrackLength(track: Track): number | null { + public static getTrackLength(track: Track): number { // load track data const trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(track.data) - if (trackData == null) { - logger.error(`Could not parse track data of track with id ${track.uid}.`) - return null - } // get last points track kilometer const trackPointsLength = trackData.features.length const trackLength = GeoJSONUtils.getTrackKm(trackData.features[trackPointsLength - 1]) - if (trackLength == null) { - logger.error(`Could not access track kilometer value of last track point of track with id ${track.uid}.`) - return null - } return trackLength } /** * Wrapper for converting internal presentation of track data as points to a linestring * @param track `Track` to get linestring for - * @returns GeoJSON feature of a linestring. This only contains pure coordinates (i.e. no property values). `null` if an error occured. + * @returns GeoJSON feature of a linestring. This only contains pure coordinates (i.e. no property values). + * @throws `HTTPError`, if the track data could not be parsed */ - public static getTrackAsLineString(track: Track): GeoJSON.Feature | null { + public static getTrackAsLineString(track: Track): GeoJSON.Feature { const trackData = GeoJSONUtils.parseGeoJSONFeatureCollectionPoints(track.data) - if (trackData == null) { - logger.error(`Could not parse track data of track with id ${track.uid}.`) - return null - } return turfHelpers.lineString(turfMeta.coordAll(trackData)) } } diff --git a/Server/src/services/user.service.ts b/Server/src/services/user.service.ts index 5b5ff500..70bb0895 100644 --- a/Server/src/services/user.service.ts +++ b/Server/src/services/user.service.ts @@ -5,6 +5,7 @@ import CryptoService from "./crypto.service" import database from "./database.service" import { z } from "zod" import { HTTPError } from "../models/error" +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library" /** * Service for user management @@ -18,7 +19,16 @@ export default class UserService { * @throws HTTPError if password could not be hashed or PrismaError if user could not be created */ public static async createUser(name: string, password: string): Promise { - await database.users.getByUsername(name) + // Pre-check whether a user with that username already exists, so we expect a prisma error to be thrown + try { + await database.users.getByUsername(name) + throw new HTTPError("The user does already exist", 409) + } catch (err) { + // Throw if any other error than expected 'not found errors' are thrown + if (!(err instanceof PrismaClientKnownRequestError) || !["P2001", "P2018", "P2021", "P2022", "P2025"].includes(err.code)) { + throw err + } + } const hashed_pass: string = await CryptoService.produceHash(password) diff --git a/Server/src/services/vehicle.service.ts b/Server/src/services/vehicle.service.ts index b313e177..3adcfab2 100644 --- a/Server/src/services/vehicle.service.ts +++ b/Server/src/services/vehicle.service.ts @@ -23,11 +23,11 @@ import { Feature, GeoJsonProperties, Point } from "geojson" export type VehicleData = { vehicle: Vehicle position: GeoJSON.Feature - trackKm?: number - percentagePosition?: number + trackKm: number + percentagePosition: number speed: number heading: number - direction?: -1 | 1 + direction: -1 | 1 } /** Service for vehicle management. */ @@ -52,6 +52,16 @@ export default class VehicleService { return filteredVehicles } + /** + * Append log for a given vehicle + * @param vehicleId vehicle id to append the log for + * @param position position of the vehicle + * @param heading heading of the vehicle + * @param speed speed of the vehicle + * @param timestamp timestamp of the gps position + * @returns appended log if successful + * @throws PrismaError, if appending log in the database was not possible + */ public static async appendLog( vehicleId: number, position: z.infer, @@ -74,6 +84,17 @@ export default class VehicleService { * @returns current `VehicleData` of `vehicle`, if no recent logs from an app are available `direction`, `trackKm` * and `percentagePosition` are not set and `heading` and `speed` are just from the tracker, also `position` is not * mapped on the track + * @throws `HTTPError` + * - if there are no recent logs of an app and no tracker logs at all + * - if the position could not be computed + * - if the track kilometer value of the position could not be accessed + * - if the tracker position could not be parsed + * - if the position of the vehicle as percentage on the track could not be computed + * - if the position could not be projected onto the track + * - if the travelling direction could not be computed + * @throws PrismaError + * - if track of vehicle could not be accessed in the database + * - if no last log of an existing tracker could be found in the database */ public static async getVehicleData(vehicle: Vehicle): Promise { // initialize track @@ -108,9 +129,6 @@ export default class VehicleService { let position: Feature | null = null if (trackerLogs.length > 0) { position = GeoJSONUtils.parseGeoJSONFeaturePoint(trackerLogs[0].position) - if (position == null) { - throw new HTTPError(`Could not parse position ${JSON.stringify(trackerLogs[0].position)}.`, 500) - } } // get heading and speed @@ -127,6 +145,7 @@ export default class VehicleService { position = TrackService.getProjectedPointOnTrack(position, track) } else { // compute position and track kilometer as well as percentage value + // TODO: try-catch and fallback (should be done in #169) position = this.computeVehiclePosition(trackerLogs, appLogs, heading, speed, track) } @@ -135,22 +154,12 @@ export default class VehicleService { } const trackKm = GeoJSONUtils.getTrackKm(position) - if (trackKm == null) { - throw new HTTPError(`Could not get track kilometer of position ${JSON.stringify(position)}.`, 500) - } - const percentagePosition = TrackService.getTrackKmAsPercentage(trackKm, track) - if (percentagePosition == null) { - throw new HTTPError( - `Could not compute percentage position for track kilometer ${trackKm} on track with id ${track.uid}.`, - 500 - ) - } return { vehicle, position, trackKm, - percentagePosition, + percentagePosition: TrackService.getTrackKmAsPercentage(trackKm, track), heading, speed, direction: this.computeVehicleTravellingDirection(trackKm, heading, track) @@ -165,7 +174,13 @@ export default class VehicleService { * @param vehicleSpeed heading of vehicle (>= 0), can be obtained with `getVehicleSpeed` * @param track `Track` assigned to the vehicle * @returns computed position of the vehicle based on log data, besides the GeoJSON point there is - * also the track kilometer in the returned GeoJSON properties field (could be null if an error occurs) + * also the track kilometer in the returned GeoJSON properties field + * @throws `HTTPError` + * - if the linestring of `track` could not be computed + * - if the tracker position could not be parsed + * - if adding weights to logs was not possible + * - if the weighted logs could not be converted to weighted track kilometer values + * - if averaging weighted logs was not possible */ private static computeVehiclePosition( trackerLogs: Log[], @@ -173,13 +188,8 @@ export default class VehicleService { vehicleHeading: number, vehicleSpeed: number, track: Track - ): GeoJSON.Feature | null { + ): GeoJSON.Feature { const lineStringData = TrackService.getTrackAsLineString(track) - if (lineStringData == null) { - logger.error(`Could not convert track with id ${track.uid} to a linestring.`) - // fallback - return GeoJSONUtils.parseGeoJSONFeaturePoint(trackerLogs[0].position) - } // add a weight to the tracker logs const weightedTrackerLogs = this.addWeightToLogs(trackerLogs, lineStringData) @@ -192,17 +202,17 @@ export default class VehicleService { // now it is unlikely, that weights can be added to the app logs, but we could at least try it logger.warn(`Could not add any weights to tracker logs.`) } else { - const tempWeightedTrackKm = this.weightedLogsToWeightedTrackKm( - weightedTrackerLogs, - vehicleSpeed, - vehicleHeading, - track - ) - if (tempWeightedTrackKm == null) { - // (if this does not work we can still try app logs, though it is also unlikely to work) - logger.warn(`Could not convert weighted tracker logs to weighted track kilometers.`) - } else { + let tempWeightedTrackKm + try { + tempWeightedTrackKm = this.weightedLogsToWeightedTrackKm( + weightedTrackerLogs, + vehicleSpeed, + vehicleHeading, + track + ) weightedTrackKm.push(...tempWeightedTrackKm) + } catch (err) { + logger.warn(`Could not convert weighted tracker logs to weighted track kilometers.`) } } @@ -212,16 +222,16 @@ export default class VehicleService { logger.warn(`Could not add any weights to app logs.`) } else { // try adding them to the list as well - const tempWeightedTrackKm = this.weightedLogsToWeightedTrackKm( - weightedAppLogs, - vehicleSpeed, - vehicleHeading, - track - ) - if (tempWeightedTrackKm == null) { - logger.warn(`Could not convert weighted app logs to weighted track kilometers.`) - } else { + try { + const tempWeightedTrackKm = this.weightedLogsToWeightedTrackKm( + weightedAppLogs, + vehicleSpeed, + vehicleHeading, + track + ) weightedTrackKm.push(...tempWeightedTrackKm) + } catch (err) { + logger.warn(`Could not convert weighted app logs to weighted track kilometers.`) } } @@ -233,10 +243,6 @@ export default class VehicleService { // build average track kilometer value const avgTrackKm = this.averageWeightedTrackKmValues(weightedTrackKm) - if (avgTrackKm == null) { - logger.info(`Could not compute average track kilometer. Perhaps the logs were not recent or accurate enough.`) - return GeoJSONUtils.parseGeoJSONFeaturePoint(trackerLogs[0].position) - } // in the end we just need to turn the track kilometer into a position again const avgPosition = along(lineStringData, avgTrackKm) @@ -251,6 +257,10 @@ export default class VehicleService { * @param vehicleHeading heading of vehicle (0-359), can be obtained with `getVehicleHeading` * @param track related track of `weightedLogs` * @returns list of weighted track kilometer values, could be less than count of `weightedLogs` (and even 0) if an error occurs + * @throws `HTTPError` + * - if no weighted log is given + * - if track kilometer value could not be accessed from a log + * - if the travelling direction could not be computed */ private static weightedLogsToWeightedTrackKm( weightedLogs: [Log, number][], @@ -260,7 +270,7 @@ export default class VehicleService { ): [number, number][] { // just need to check this for the next step if (weightedLogs.length === 0) { - return [] + throw new HTTPError(`Expected at least one log for converting to track kilometer`, 500) } // vehicle should be the same for all logs @@ -278,8 +288,10 @@ export default class VehicleService { } // get last known track kilometer - const lastTrackKm = this.getTrackKmFromLog(weightedLogs[i][0], track) - if (lastTrackKm == null) { + let lastTrackKm + try { + lastTrackKm = this.getTrackKmFromLog(weightedLogs[i][0], track) + } catch (err) { logger.warn(`Could not compute last track kilometer for last log with id ${weightedLogs[i][0].uid}.`) continue } @@ -300,26 +312,17 @@ export default class VehicleService { * Compute track kilometer for a position of a given log * @param log `Log` to compute track kilometer for * @param track related track of `log` - * @returns track kilometer value for `log`, `null` if an error occurs + * @returns track kilometer value for `log` + * @throws `HTTPError` + * - if the position of `log` could not be parsed + * - if the track kilometer value of the position of `log` could not be computed */ - private static getTrackKmFromLog(log: Log, track: Track): number | null { + private static getTrackKmFromLog(log: Log, track: Track): number { // get position from log const logPosition = GeoJSONUtils.parseGeoJSONFeaturePoint(log.position) - if (logPosition == null) { - logger.error(`Position ${JSON.stringify(log.position)} could not be parsed.`) - return null - } // compute track kilometer for this position - const logTrackKm = TrackService.getPointTrackKm(logPosition, track) - if (logTrackKm == null) { - logger.error( - `Could not compute track kilometer for position ${JSON.stringify(logPosition)} and track with id ${track.uid}.` - ) - return null - } - - return logTrackKm + return TrackService.getPointTrackKm(logPosition, track) } /** @@ -330,6 +333,7 @@ export default class VehicleService { * @param distanceCutoff value to cut the distance / accuracy factor off at, default is 50 meters (recommended for tracker logs) * @param averaging flag to decide wether all Logs should be averaged via their related weight * @returns list of `Log`s, each associated with a weight, could be less than count of `logs` (and even 0) if an error occurs + * @throws `HTTPError`, if a log position could not be parsed */ private static addWeightToLogs( logs: Log[], @@ -351,8 +355,10 @@ export default class VehicleService { timeWeight = timeWeight > 0 ? timeWeight : 0 // get position from log - const logPosition = GeoJSONUtils.parseGeoJSONFeaturePoint(logs[i].position) - if (logPosition == null) { + let logPosition + try { + logPosition = GeoJSONUtils.parseGeoJSONFeaturePoint(logs[i].position) + } catch (err) { logger.warn(`Position ${JSON.stringify(logs[i].position)} could not be parsed.`) continue } @@ -379,16 +385,21 @@ export default class VehicleService { /** * Build average of weighted track kilometer values * @param weightedTrackKms list of track kilometer values (first) with their related positive weight (second) - * @returns averaged track kilometer value of `weightedTrackKms`, null if an error occurs + * @returns averaged track kilometer value of `weightedTrackKms` + * @throws `HTTPError` + * - if there is a negative weight + * - if there was no weight greater than 0 */ - private static averageWeightedTrackKmValues(weightedTrackKms: [number, number][]): number | null { + private static averageWeightedTrackKmValues(weightedTrackKms: [number, number][]): number { // calculate total of all weights let weightSum = 0 for (let i = 0; i < weightedTrackKms.length; i++) { // check if weight is negative (this could result in unwanted behaviour) if (weightedTrackKms[i][1] < 0) { - logger.error(`Expected positive weights for track kilometer values only, but got ${weightedTrackKms[i][1]}.`) - return null + throw new HTTPError( + `Expected positive weights for track kilometer values only, but got ${weightedTrackKms[i][1]}.`, + 500 + ) } weightSum += weightedTrackKms[i][1] @@ -396,8 +407,10 @@ export default class VehicleService { // avoid divide by zero if (weightSum == 0) { - logger.error(`All weights for track kilometers were 0`) - return null + throw new HTTPError( + `Expected at least one weight to be greater than 0 while computing average track kilometer.`, + 500 + ) } // normalizing weights and averaging track kilometer values @@ -442,19 +455,16 @@ export default class VehicleService { /** * Determine travelling direction of a vehicle related to its track (either "forward" or "backward") - * @param vehicleHeading heading of vehicle (0-359), can be obtained with `getVehicleHeading` * @param trackKm track kilometer at which the vehicle currently is (can be found with `VehicleService.getVehicleTrackDistanceKm`) + * @param vehicleHeading heading of vehicle (0-359), can be obtained with `getVehicleHeading` * @param track track to compute the direction of a vehicle with * @returns 1 or -1 if the vehicle is heading towards the end and start of the track respectively + * @throws `HTTPError`, if the heading of the track at `trackKm` could not be computed */ private static computeVehicleTravellingDirection(trackKm: number, vehicleHeading: number, track: Track): 1 | -1 { // TODO: needs improvements (probably with #118 together), should be independent from track kilometer (position needs to be computed for that) // compute track heading const trackBearing = TrackService.getTrackHeading(track, trackKm) - // TODO - if (trackBearing == null) { - throw new HTTPError(`Could not compute heading of track with id ${track.uid} at track kilometer ${trackKm}.`, 500) - } // TODO: maybe give this a buffer of uncertainty if (vehicleHeading - trackBearing < 90 || vehicleHeading - trackBearing > -90) { return 1 diff --git a/Server/src/utils/geojsonUtils.ts b/Server/src/utils/geojsonUtils.ts index 3de631e5..b43972a3 100644 --- a/Server/src/utils/geojsonUtils.ts +++ b/Server/src/utils/geojsonUtils.ts @@ -1,3 +1,5 @@ +import { HTTPError } from "../models/error" + /** * Some utilities for simpler handling of GeoJSON */ @@ -21,11 +23,12 @@ export default class GeoJSONUtils { /** * Get track kilometer for given GeoJSON point (basically a wrapper for accessing this property) * @param point GeoJSON point to get the track kilometer for - * @returns track kilometer if available, `null` otherwise + * @returns track kilometer if available + * @throws `HTTPError`, if track kilometer value is not set */ - public static getTrackKm(point: GeoJSON.Feature): number | null { + public static getTrackKm(point: GeoJSON.Feature): number { if (point.properties == null || point.properties["trackKm"] == null) { - return null + throw new HTTPError(`Could not get track kilometer of position ${JSON.stringify(point)}.`, 500) } return point.properties["trackKm"] } @@ -87,37 +90,39 @@ export default class GeoJSONUtils { /** * Parses JSON to a GeoJSON feature of a point (if possible) - * @param json JSON to parse - * @returns parsed GeoJSON feature or `null` if an error occured while parsing + * @param object JSON to parse + * @returns parsed GeoJSON feature + * @throws `HTTPError`, if parsing was not possible */ - public static parseGeoJSONFeaturePoint(json: unknown): GeoJSON.Feature | null { - if (this.isGeoJSONFeaturePoint(json)) { - return json as GeoJSON.Feature - } else if (this.isGeoJSONPosition(json)) { + public static parseGeoJSONFeaturePoint(object: unknown): GeoJSON.Feature { + if (this.isGeoJSONFeaturePoint(object)) { + return object as GeoJSON.Feature + } else if (this.isGeoJSONPosition(object)) { // If we just have plain 2D coordinates, construct a point feature. const feature: GeoJSON.Feature = { type: "Feature", properties: {}, geometry: { type: "Point", - coordinates: json + coordinates: object } } return feature } - return null + throw new HTTPError(`Could not parse ${JSON.stringify(object)} as GeoJSON feature of point.`, 500) } /** * Try to parse anything to a GeoJSON feature collection of points (if possible) * @param object object to parse - * @returns parsed GeoJSON feature collection or `null` if an error occured while parsing + * @returns parsed GeoJSON feature collection + * @throws `HTTPError`, if parsing was not possible */ - public static parseGeoJSONFeatureCollectionPoints(object: unknown): GeoJSON.FeatureCollection | null { + public static parseGeoJSONFeatureCollectionPoints(object: unknown): GeoJSON.FeatureCollection { if (this.isGeoJSONFeatureCollectionPoints(object)) { return object as GeoJSON.FeatureCollection } - return null + throw new HTTPError(`Could not parse ${JSON.stringify(object)} as GeoJSON feature collection of points.`, 500) } /**