From b1ac427c670f27950ea32f1d45e5b4da9adfd7c7 Mon Sep 17 00:00:00 2001 From: Liam Boddin Date: Wed, 30 Aug 2023 20:20:04 +0200 Subject: [PATCH 01/20] Converted icon to new functionality as a number for the API type but string for the database type --- Server/src/models/api.ts | 2 +- Server/src/routes/init.route.ts | 11 ++++++++--- Server/src/routes/poitype.route.ts | 10 +++++----- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Server/src/models/api.ts b/Server/src/models/api.ts index 28f009bb..8f9adebc 100644 --- a/Server/src/models/api.ts +++ b/Server/src/models/api.ts @@ -40,7 +40,7 @@ export type TrackList = BareTrack[] /** @see {isCreatePOIType} ts-auto-guard:type-guard */ export type CreatePOIType = { name: string - icon: string + icon: number description?: string } diff --git a/Server/src/routes/init.route.ts b/Server/src/routes/init.route.ts index be964f4f..16893796 100644 --- a/Server/src/routes/init.route.ts +++ b/Server/src/routes/init.route.ts @@ -4,7 +4,7 @@ import { InitRequestApp, InitResponseApp, TrackListEntryApp } from "../models/ap import { PointOfInterest, 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 VehicleService from "../services/vehicle.service" import { Feature, FeatureCollection, GeoJsonProperties, LineString, Point } from "geojson" @@ -199,11 +199,16 @@ 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 POIService.getPOITypeById(poi.typeId) if (!type) { logger.error(`Could not determine type of poi with id ${poi.uid}`) continue } + const typeEnum: number = Number.parseInt(type.icon) + if (!Number.isFinite(typeEnum)) { + logger.error(`Icon of type with id ${type.uid} is not a finite number.`) + continue + } const geoJsonPos: Feature | null = GeoJSONUtils.parseGeoJSONFeaturePoint(poi.position) if (!geoJsonPos) { @@ -223,7 +228,7 @@ export class InitRoute { apiPois.push({ id: poi.uid, name: poi.name, - typeId: 0 <= type && type <= 4 ? type : 0, + typeId: 0 <= typeEnum && typeEnum <= 5 ? typeEnum : 0, pos: pos, percentagePosition: percentagePosition, isTurningPoint: poi.isTurningPoint, diff --git a/Server/src/routes/poitype.route.ts b/Server/src/routes/poitype.route.ts index 980bf20c..6abb4ab1 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) @@ -84,7 +84,7 @@ export class PoiTypeRoute { private async createType(req: Request, res: Response): Promise { 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.toString(), description) if (!poiType) { logger.error("Could not create poi type") res.sendStatus(500) @@ -94,7 +94,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) @@ -116,7 +116,7 @@ export class PoiTypeRoute { return } - type = await database.pois.updateType(typeId, userData.name, userData.icon, userData.description) + type = await database.pois.updateType(typeId, userData.name, userData.icon.toString(), userData.description) if (!type) { logger.error(`Could not update poi type with id ${userData.id}`) res.sendStatus(500) From 06e22b252c9e014565f981fe325b92f4fa50291b Mon Sep 17 00:00:00 2001 From: Liam Boddin Date: Thu, 31 Aug 2023 17:50:58 +0200 Subject: [PATCH 02/20] Added JSDoc to crypto.service.ts --- Server/src/services/crypto.service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 { From ab3a944e5d42f55c59cf69ea89aa0d24f4b5cf9a Mon Sep 17 00:00:00 2001 From: Liam Boddin Date: Thu, 31 Aug 2023 17:52:21 +0200 Subject: [PATCH 03/20] Remove unnecessary methods from poi service --- Server/src/routes/poi.route.ts | 8 +- Server/src/services/poi.service.ts | 218 ++++------------------------- 2 files changed, 28 insertions(+), 198 deletions(-) diff --git a/Server/src/routes/poi.route.ts b/Server/src/routes/poi.route.ts index 433643ef..a58198a0 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) @@ -233,13 +233,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/services/poi.service.ts b/Server/src/services/poi.service.ts index 3ee5e423..baf331e7 100644 --- a/Server/src/services/poi.service.ts +++ b/Server/src/services/poi.service.ts @@ -3,8 +3,6 @@ import database from "./database.service" import TrackService from "./track.service" import VehicleService from "./vehicle.service" import GeoJSONUtils from "../utils/geojsonUtils" - -import distance from "@turf/distance" import { logger } from "../utils/logger" /** @@ -78,15 +76,6 @@ export default class POIService { 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 @@ -167,6 +156,27 @@ export default class POIService { return (poiDistKm / trackLength) * 100 } + /** + * Search for POI's on a track + * @param track `Track` to search on for POI's + * @param type `POIType` to filter the returned POI's by + * @returns `POI[]` of all POI's along the given `track` + */ + public static async getAllPOIsForTrack(track: Track, type?: POIType): Promise { + // no type given, just return database query + if (type == null) { + return database.pois.getAll(track.uid) + } + + // filter by type + const trackPOIs = await database.pois.getAll(track.uid) + trackPOIs.filter(function(poi, _index, _poiList) { + return poi.typeId == type.uid + }) + return trackPOIs + } + +// THIS METHOD IS NOT USED ANYMORE BUT HAS SOME LOGICS IN IT /** * Search for nearby POI's either within a certain distance or by amount * @param point point to search nearby POI's from @@ -236,7 +246,7 @@ export default class POIService { return null } - allPOIsForTrack.filter(async function (poi, _index, _pois) { + allPOIsForTrack.filter(async function(poi, _index, _pois) { const poiTrackKm = await POIService.getPOITrackDistanceKm(poi) if (poiTrackKm == null) { return false @@ -247,7 +257,7 @@ export default class POIService { // filter pois by distance if given if (maxDistance != null) { - allPOIsForTrack.filter(async function (poi, _index, _pois) { + allPOIsForTrack.filter(async function(poi, _index, _pois) { const poiTrackKm = await POIService.getPOITrackDistanceKm(poi) if (poiTrackKm == null) { return false @@ -257,7 +267,7 @@ export default class POIService { }) } // sort POI's by distance to searched point - allPOIsForTrack = allPOIsForTrack.sort(function (poi0, poi1) { + allPOIsForTrack = allPOIsForTrack.sort(function(poi0, poi1) { // parse POI position const POIPos0 = GeoJSONUtils.parseGeoJSONFeaturePoint(poi0.position) const POIPos1 = GeoJSONUtils.parseGeoJSONFeaturePoint(poi1.position) @@ -292,184 +302,4 @@ export default class POIService { allPOIsForTrack.slice(0, count) return allPOIsForTrack } - - /** - * Search for POI's on a track - * @param track `Track` to search on for POI's - * @param type `POIType` to filter the returned POI's by - * @returns `POI[]` of all POI's along the given `track` - */ - public static async getAllPOIsForTrack(track: Track, type?: POIType): Promise { - // no type given, just return database query - if (type == null) { - return database.pois.getAll(track.uid) - } - - // filter by type - const trackPOIs = await database.pois.getAll(track.uid) - 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 - } - return database.pois.update(poi.uid, undefined, undefined, undefined, undefined, enrichedPoint) - } - - /** - * 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, 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, undefined, 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, undefined, undefined, 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 - return database.pois.update(poi.uid, undefined, undefined, undefined, track.uid, updatedPOIPos) - } - - /** - * 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, undefined, undefined, undefined, undefined, undefined, 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(type, icon, 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, 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, undefined, undefined, 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, undefined, 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) - } } From dd8ff11a1c11171d5cb9386e15bb058b30e74385 Mon Sep 17 00:00:00 2001 From: Liam Boddin Date: Thu, 31 Aug 2023 17:53:47 +0200 Subject: [PATCH 04/20] Remove unnecessary methods from track service --- Server/src/services/track.service.ts | 56 +--------------------------- 1 file changed, 1 insertion(+), 55 deletions(-) diff --git a/Server/src/services/track.service.ts b/Server/src/services/track.service.ts index 08eb0354..743c1e2b 100644 --- a/Server/src/services/track.service.ts +++ b/Server/src/services/track.service.ts @@ -57,7 +57,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] @@ -265,58 +265,4 @@ export default class TrackService { } 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 = this.enrichTrackData(path) - // typecast to any, because JSON is expected - // typecast to any, because JSON is expected - return database.tracks.update(track.uid, undefined, undefined, enrichedTrack as any) - } - - /** - * 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, 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, undefined, 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) - } } From fd6c5d3d62a5da080fcbf668747d139640250fed Mon Sep 17 00:00:00 2001 From: Liam Boddin Date: Thu, 31 Aug 2023 17:55:22 +0200 Subject: [PATCH 05/20] Remove unnecessary methods from vehicle- and tracker service --- Server/src/routes/track.route.ts | 11 +- Server/src/routes/tracker.route.ts | 5 +- Server/src/routes/vehicle.route.ts | 6 +- Server/src/services/tracker.service.ts | 113 +---------------- Server/src/services/vehicle.service.ts | 161 ++----------------------- 5 files changed, 18 insertions(+), 278 deletions(-) diff --git a/Server/src/routes/track.route.ts b/Server/src/routes/track.route.ts index b6f1133a..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. @@ -221,9 +220,9 @@ export class TrackRoute { // If we know that, convert it in the API format. const pos: Position | undefined = geo_pos ? { - lat: GeoJSONUtils.getLatitude(geo_pos), - lng: GeoJSONUtils.getLongitude(geo_pos) - } + lat: GeoJSONUtils.getLatitude(geo_pos), + lng: GeoJSONUtils.getLongitude(geo_pos) + } : undefined // Also acquire the percentage position. It might happen that a percentage position is known, while the position is not. // This might not make much sense. @@ -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 727e0e89..544ab54a 100644 --- a/Server/src/routes/tracker.route.ts +++ b/Server/src/routes/tracker.route.ts @@ -5,7 +5,6 @@ import TrackerService from "../services/tracker.service" import { UplinkTracker } from "../models/api.tracker" import please_dont_crash from "../utils/please_dont_crash" import { Tracker, Vehicle } from "@prisma/client" -import VehicleService from "../services/vehicle.service" import database from "../services/database.service" import { Tracker as APITracker } from "../models/api" @@ -152,7 +151,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) @@ -165,7 +164,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/vehicle.route.ts b/Server/src/routes/vehicle.route.ts index df599dc0..18859e28 100644 --- a/Server/src/routes/vehicle.route.ts +++ b/Server/src/routes/vehicle.route.ts @@ -47,11 +47,11 @@ export class VehicleRoute { /** * 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 => { @@ -320,7 +320,7 @@ export class VehicleRoute { return } - const userVehicle: Vehicle | null = await VehicleService.getVehicleById(userData.vehicleId) + 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) diff --git a/Server/src/services/tracker.service.ts b/Server/src/services/tracker.service.ts index 9b11b129..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 { Prisma, Log, 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(trackerId, null, data) - } 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, 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 d9a60166..853af1ba 100644 --- a/Server/src/services/vehicle.service.ts +++ b/Server/src/services/vehicle.service.ts @@ -1,13 +1,10 @@ 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" -import * as turfHelpers from "@turf/helpers" -import * as turfMeta from "@turf/meta" import nearestPointOnLine from "@turf/nearest-point-on-line" import { Position } from "../models/api" @@ -29,36 +26,6 @@ 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(type.uid, track_uid, name) - } - - /** - * 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 @@ -128,7 +95,7 @@ export default class VehicleService { return null } - allVehiclesOnTrack.filter(async function (vehicle, index, vehicles) { + allVehiclesOnTrack.filter(async function(vehicle, index, vehicles) { const vehicleTrackKm = await VehicleService.getVehicleTrackDistanceKm(vehicle) if (vehicleTrackKm == null) { // TODO: log this @@ -140,7 +107,7 @@ export default class VehicleService { // filter vehicles by distance if given if (maxDistance != null) { - allVehiclesOnTrack.filter(async function (vehicle, index, vehicles) { + allVehiclesOnTrack.filter(async function(vehicle, index, vehicles) { const vehicleTrackKm = await VehicleService.getVehicleTrackDistanceKm(vehicle) if (vehicleTrackKm == null) { return false @@ -151,7 +118,7 @@ export default class VehicleService { // enrich vehicles with track distance for sorting let vehiclesWithDistances: [Vehicle, number][] = await Promise.all( - allVehiclesOnTrack.map(async function (vehicle) { + allVehiclesOnTrack.map(async function(vehicle) { let vehicleDistance = await VehicleService.getVehicleTrackDistanceKm(vehicle) vehicleDistance = vehicleDistance == null ? -1 : vehicleDistance // this should not happen return [vehicle, vehicleDistance] @@ -159,7 +126,7 @@ export default class VehicleService { ) // sort vehicles by distance to searched point - vehiclesWithDistances = vehiclesWithDistances.sort(function (v0, v1) { + vehiclesWithDistances = vehiclesWithDistances.sort(function(v0, v1) { // if this happens, we cannot sort the POI's if (v0[1] < 0 || v1[1] < 0) { // TODO: log this, maybe some other handling @@ -202,7 +169,7 @@ export default class VehicleService { // get all vehicles for track and filter by type const vehicles: Vehicle[] = await database.vehicles.getAll(track.uid) - const filteredVehicles = vehicles.filter(function (vehicle, index, vehicles) { + const filteredVehicles = vehicles.filter(function(vehicle, index, vehicles) { return vehicle.typeId == type.uid }) return filteredVehicles @@ -253,7 +220,7 @@ export default class VehicleService { // add predicted tracker positions for (let i = 0; i < trackers.length; i++) { // create list of positions of this specific tracker (also filtered for last ten minutes) - const trackerLogs = allLogs.filter(function (log) { + const trackerLogs = allLogs.filter(function(log) { return log.trackerId == trackers[i].uid }) @@ -617,118 +584,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, undefined, undefined, 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, 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(type, icon, 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, 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, undefined, undefined, 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, undefined, 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 - } } From a86a86596226618d982ffe196cb9e53e82200994 Mon Sep 17 00:00:00 2001 From: Kevin Ebsen Date: Thu, 31 Aug 2023 21:45:30 +0200 Subject: [PATCH 06/20] #91: Fixed mutation of certain array functions --- Server/src/services/poi.service.ts | 10 +++++----- Server/src/services/vehicle.service.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Server/src/services/poi.service.ts b/Server/src/services/poi.service.ts index 3ee5e423..66de8fc6 100644 --- a/Server/src/services/poi.service.ts +++ b/Server/src/services/poi.service.ts @@ -236,7 +236,7 @@ export default class POIService { return null } - allPOIsForTrack.filter(async function (poi, _index, _pois) { + allPOIsForTrack = allPOIsForTrack.filter(async function (poi, _index, _pois) { const poiTrackKm = await POIService.getPOITrackDistanceKm(poi) if (poiTrackKm == null) { return false @@ -247,7 +247,7 @@ export default class POIService { // filter pois by distance if given if (maxDistance != null) { - allPOIsForTrack.filter(async function (poi, _index, _pois) { + allPOIsForTrack = allPOIsForTrack.filter(async function (poi, _index, _pois) { const poiTrackKm = await POIService.getPOITrackDistanceKm(poi) if (poiTrackKm == null) { return false @@ -289,7 +289,7 @@ export default class POIService { } // only return first #count of POI's - allPOIsForTrack.slice(0, count) + allPOIsForTrack = allPOIsForTrack.slice(0, count) return allPOIsForTrack } @@ -306,8 +306,8 @@ 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 diff --git a/Server/src/services/vehicle.service.ts b/Server/src/services/vehicle.service.ts index d9a60166..95d21b9f 100644 --- a/Server/src/services/vehicle.service.ts +++ b/Server/src/services/vehicle.service.ts @@ -128,7 +128,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 @@ -140,7 +140,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 @@ -184,7 +184,7 @@ export default class VehicleService { } // only return first #count of POI's - allVehiclesOnTrack.slice(0, count) + allVehiclesOnTrack = allVehiclesOnTrack.slice(0, count) return allVehiclesOnTrack } From c8bd8edd5cd2f48dcf8fcad4c40165d68ea8db97 Mon Sep 17 00:00:00 2001 From: Daniel Maeckelmann Date: Fri, 1 Sep 2023 11:22:15 +0200 Subject: [PATCH 07/20] Introduced POI Icon Enum --- Server/src/models/api.ts | 12 +++++++++++- Server/src/routes/init.route.ts | 16 +++++++++++----- Server/src/routes/poitype.route.ts | 2 ++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Server/src/models/api.ts b/Server/src/models/api.ts index 8f9adebc..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: number + icon: POITypeIcon description?: string } diff --git a/Server/src/routes/init.route.ts b/Server/src/routes/init.route.ts index 16893796..4e47c866 100644 --- a/Server/src/routes/init.route.ts +++ b/Server/src/routes/init.route.ts @@ -1,7 +1,7 @@ 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, POIType, Track } from "@prisma/client" @@ -204,11 +204,17 @@ export class InitRoute { logger.error(`Could not determine type of poi with id ${poi.uid}`) continue } - const typeEnum: number = Number.parseInt(type.icon) - if (!Number.isFinite(typeEnum)) { - logger.error(`Icon of type with id ${type.uid} is not a finite number.`) + 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) { @@ -228,7 +234,7 @@ export class InitRoute { apiPois.push({ id: poi.uid, name: poi.name, - typeId: 0 <= typeEnum && typeEnum <= 5 ? typeEnum : 0, + typeId: appType, pos: pos, percentagePosition: percentagePosition, isTurningPoint: poi.isTurningPoint, diff --git a/Server/src/routes/poitype.route.ts b/Server/src/routes/poitype.route.ts index 6abb4ab1..5321c4cd 100644 --- a/Server/src/routes/poitype.route.ts +++ b/Server/src/routes/poitype.route.ts @@ -82,6 +82,7 @@ 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.toString(), description) @@ -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 From f2ccc50a47251141b623acb243f10918960f32c1 Mon Sep 17 00:00:00 2001 From: Daniel Maeckelmann Date: Fri, 1 Sep 2023 17:48:30 +0200 Subject: [PATCH 08/20] Made poi type icons updatable --- Server/src/services/db/poi.controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/Server/src/services/db/poi.controller.ts b/Server/src/services/db/poi.controller.ts index 7d5b5d8e..98a3632e 100644 --- a/Server/src/services/db/poi.controller.ts +++ b/Server/src/services/db/poi.controller.ts @@ -71,6 +71,7 @@ export default class POIController { }, data: { name: name, + icon: icon, description: description } }) From 2271949a8e67d21d3489f404162e0649045ef97b Mon Sep 17 00:00:00 2001 From: Daniel Maeckelmann Date: Fri, 1 Sep 2023 17:59:30 +0200 Subject: [PATCH 09/20] Frontend: Added POI Type icon support and updated POI icons --- Website/public/generic_rail_bound_vehicle.svg | 4 - Website/public/poiTypeIcons/generic.svg | 15 +++ .../generic_rail_bound_vehicle.svg | 4 - .../poiTypeIcons/lesser_level_crossing.svg | 16 +++- .../public/poiTypeIcons/level_crossing.svg | 20 +++- Website/public/poiTypeIcons/parking.svg | 4 - .../public/poiTypeIcons/passing_position.svg | 49 ++++++++++ Website/public/poiTypeIcons/picnic.svg | 20 ++++ Website/public/poiTypeIcons/track_end.svg | 47 ++++++++++ Website/public/poiTypeIcons/turning_point.svg | 22 +++++ .../vehicle/Vehicle_background_neutral.svg | 2 +- ...ground_heading.svg => Vehicle_heading.svg} | 1 - Website/src/app/components/iconSelection.tsx | 37 ++++---- Website/src/app/components/login_wrap.tsx | 56 +++++++---- Website/src/app/components/map.tsx | 18 +++- .../src/app/management/poiTypes/client.tsx | 10 +- Website/src/utils/api.ts | 15 ++- Website/src/utils/common.ts | 29 +++++- Website/src/utils/rotatingIcon.ts | 93 ++++++++++--------- 19 files changed, 344 insertions(+), 118 deletions(-) delete mode 100644 Website/public/generic_rail_bound_vehicle.svg create mode 100644 Website/public/poiTypeIcons/generic.svg delete mode 100644 Website/public/poiTypeIcons/generic_rail_bound_vehicle.svg delete mode 100644 Website/public/poiTypeIcons/parking.svg create mode 100644 Website/public/poiTypeIcons/passing_position.svg create mode 100644 Website/public/poiTypeIcons/picnic.svg create mode 100644 Website/public/poiTypeIcons/track_end.svg create mode 100644 Website/public/poiTypeIcons/turning_point.svg rename Website/public/vehicle/{Vehicle_background_heading.svg => Vehicle_heading.svg} (77%) 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/iconSelection.tsx b/Website/src/app/components/iconSelection.tsx index 55c32cfc..27492971 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,29 +17,31 @@ export default function IconSelection({ id, name }: { - currentIcon: string; - setIcon: (newIcon: string) => void; + currentIcon: POITypeIcon | null; + setIcon: (newIcon: POITypeIcon | null) => 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: "", + value: null, label: (
[Bitte auswählen] @@ -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 !== null) { 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/management/poiTypes/client.tsx b/Website/src/app/management/poiTypes/client.tsx index e5f3cf56..a24fa2ba 100644 --- a/Website/src/app/management/poiTypes/client.tsx +++ b/Website/src/app/management/poiTypes/client.tsx @@ -8,7 +8,7 @@ else, but also not in ´page.tsx` as we need to obtain the currently selected tr import { FormEventHandler, useRef, useState } from "react"; import useSWR from "swr"; import { Option } from "@/utils/types"; -import { CreatePOIType, POIType } from "@/utils/api"; +import { CreatePOIType, POIType, POITypeIcon } from "@/utils/api"; import Select, { Options, SingleValue } from "react-select"; import IconSelection from "@/app/components/iconSelection"; import assert from "assert"; @@ -41,7 +41,7 @@ export default function POITypeManagement() { // Form states const [selType, setSelType] = useState(addOption); const [typeName, setTypeName] = useState(""); - const [typeIcon, setTypeIcon] = useState(""); + const [typeIcon, setTypeIcon] = useState(null as POITypeIcon | null); const [typeDescription, setTypeDescription] = useState(""); /** modified: A "dirty flag" to prevent loosing information. */ const [modified, setModified] = useState(false); @@ -57,6 +57,10 @@ export default function POITypeManagement() { // Form submission function const updateType: FormEventHandler = async e => { e.preventDefault(); + if (typeIcon == null) { + setError("Bitte wählen Sie ein Icon aus!"); + return; + } // create the corresponding payload to send to the backend. // When adding a new vehicle type, uid should be undefined, and `selType` should be an empty string const createPayload: CreatePOIType = { @@ -153,7 +157,7 @@ export default function POITypeManagement() { setSelType(newValue); // And set the form values to the properties of the newly selected vehicle type setTypeName(selectedType?.name ?? ""); - setTypeIcon(selectedType?.icon ?? ""); + setTypeIcon(selectedType?.icon ?? null); setTypeDescription("" + (selectedType?.description ?? "")); setModified(false); }; diff --git a/Website/src/utils/api.ts b/Website/src/utils/api.ts index 6918b026..7cdf047e 100644 --- a/Website/src/utils/api.ts +++ b/Website/src/utils/api.ts @@ -32,9 +32,20 @@ export type FullTrack = BareTrack & { export type TrackList = BareTrack[]; +export const POITypeIconValues = { + Generic: 0, + LevelCrossing: 1, + LesserLevelCrossing: 2, + Picnic: 3, + TrackEnd: 4, + TurningPoint: 5 +} as const; + +export type POITypeIcon = (typeof POITypeIconValues)[keyof typeof POITypeIconValues]; + export type CreatePOIType = { name: string; - icon: string; + icon: POITypeIcon; description?: string; }; @@ -105,7 +116,7 @@ export type VehicleType = UpdateVehicleType & { export type Tracker = { id: string; vehicleId: number | null; - data?: never; + data?: unknown; }; /** diff --git a/Website/src/utils/common.ts b/Website/src/utils/common.ts index 9ba19840..704298db 100644 --- a/Website/src/utils/common.ts +++ b/Website/src/utils/common.ts @@ -1,8 +1,27 @@ -import {Inter} from "next/font/google"; +import { Inter } from "next/font/google"; +import { POITypeIcon, POITypeIconValues } from "@/utils/api"; -export const inter = Inter({ subsets: ['latin'] }) +export const inter = Inter({ subsets: ["latin"] }); export const meta_info = { - title: 'RailTrail Admin Interface', - description: 'An administrative interface for the RailTrail rail vehicle management system.', -} \ No newline at end of file + title: "RailTrail Admin Interface", + description: "An administrative interface for the RailTrail rail vehicle management system." +}; + +export const POIIconImg: Readonly> = { + [POITypeIconValues.Generic]: "/poiTypeIcons/generic.svg", + [POITypeIconValues.LevelCrossing]: "/poiTypeIcons/level_crossing.svg", + [POITypeIconValues.LesserLevelCrossing]: "/poiTypeIcons/lesser_level_crossing.svg", + [POITypeIconValues.Picnic]: "/poiTypeIcons/picnic.svg", + [POITypeIconValues.TrackEnd]: "/poiTypeIcons/track_end.svg", + [POITypeIconValues.TurningPoint]: "/poiTypeIcons/turning_point.svg" +} as const; + +export const POIIconCommonName: Readonly> = { + [POITypeIconValues.Generic]: "Generisch", + [POITypeIconValues.LevelCrossing]: "Bahnübergang", + [POITypeIconValues.LesserLevelCrossing]: "Unbeschilderter Bahnübergang", + [POITypeIconValues.Picnic]: "Picknickplatz", + [POITypeIconValues.TrackEnd]: "Streckenende", + [POITypeIconValues.TurningPoint]: "Wendepunkt" +} as const; diff --git a/Website/src/utils/rotatingIcon.ts b/Website/src/utils/rotatingIcon.ts index fa7c287b..72d4d73b 100644 --- a/Website/src/utils/rotatingIcon.ts +++ b/Website/src/utils/rotatingIcon.ts @@ -1,55 +1,56 @@ - -import L from "leaflet" +import L from "leaflet"; /** * A Vehicle Icon with a rotating background layer and a foreground layer that does not rotate. */ export default class RotatingVehicleIcon extends L.DivIcon { + private readonly baseElement: HTMLDivElement; + private readonly baseLayer: HTMLImageElement; + private readonly rotatingLayer: HTMLImageElement; + private readonly foregroundLayer: HTMLImageElement; - private readonly baseElement: HTMLDivElement; - private readonly baseLayer: HTMLImageElement; - private readonly rotatingLayer: HTMLImageElement; - private readonly foregroundLayer: HTMLImageElement; - - /** - * @param baseElement The div-element to use as a base for the construction - * @param baseLayerUrl The url of the background shown when rotation is unknown - * @param rotatingLayerUrl The url of the background shown when the rotation is known. - * @param foregroundUrl The url of the non-rotating foreground image. - */ - public constructor(baseElement: HTMLDivElement, baseLayerUrl: string = '/vehicle/Vehicle_background_neutral.svg', rotatingLayerUrl: string = '/vehicle/Vehicle_background_heading.svg', foregroundUrl: string = '/vehicle/Vehicle_foreground.svg') { - super({html: baseElement, className: 'rotatingIconContainerContainer', iconSize: [45, 45]}) - this.baseElement = baseElement - this.baseElement.className = 'rotatingIconContainer' - // construct the background layers - this.baseLayer = document.createElement('img'); - this.baseLayer.src = baseLayerUrl; - this.baseLayer.className = 'rotatingIcon'; - this.rotatingLayer = document.createElement('img'); - this.rotatingLayer.src = rotatingLayerUrl; - this.rotatingLayer.className = 'rotatingIcon'; + /** + * @param baseElement The div-element to use as a base for the construction + * @param baseLayerUrl The url of the background shown when rotation is unknown + * @param rotatingLayerUrl The url of the background shown when the rotation is known. + * @param foregroundUrl The url of the non-rotating foreground image. + */ + public constructor( + baseElement: HTMLDivElement, + baseLayerUrl: string = "/vehicle/Vehicle_background_neutral.svg", + rotatingLayerUrl: string = "/vehicle/Vehicle_heading.svg", + foregroundUrl: string = "/vehicle/Vehicle_foreground.svg" + ) { + super({ html: baseElement, className: "rotatingIconContainerContainer", iconSize: [45, 45] }); + this.baseElement = baseElement; + this.baseElement.className = "rotatingIconContainer"; + // construct the background layers + this.baseLayer = document.createElement("img"); + this.baseLayer.src = baseLayerUrl; + this.baseLayer.className = "rotatingIcon"; + this.rotatingLayer = document.createElement("img"); + this.rotatingLayer.src = rotatingLayerUrl; + this.rotatingLayer.className = "rotatingIcon"; - // and the foreground layer - this.foregroundLayer = document.createElement('img'); - this.foregroundLayer.src = foregroundUrl; - this.foregroundLayer.className = 'rotatingIcon'; + // and the foreground layer + this.foregroundLayer = document.createElement("img"); + this.foregroundLayer.src = foregroundUrl; + this.foregroundLayer.className = "rotatingIcon"; - // then stack them all into the base element - this.baseElement.append(this.baseLayer, this.rotatingLayer, this.foregroundLayer) - } + // then stack them all into the base element + this.baseElement.append(this.baseLayer, this.rotatingLayer, this.foregroundLayer); + } - /** - * Sets the rotation of the background layer - * @param degrees The target rotation for the icon. - */ - public setRotation(degrees: number | undefined) { - if (degrees !== undefined) { - this.rotatingLayer.style.rotate = `${degrees}deg`; - this.baseLayer.style.display = 'none' - this.rotatingLayer.style.display = ''; - } else { - this.baseLayer.style.display = '' - this.rotatingLayer.style.display = 'none' - } - } -} \ No newline at end of file + /** + * Sets the rotation of the background layer + * @param degrees The target rotation for the icon. + */ + public setRotation(degrees: number | undefined) { + if (degrees !== undefined) { + this.rotatingLayer.style.rotate = `${degrees}deg`; + this.rotatingLayer.style.display = ""; + } else { + this.rotatingLayer.style.display = "none"; + } + } +} From b1a0164f6a12d460162c446cf2ea337558c909c2 Mon Sep 17 00:00:00 2001 From: Daniel Maeckelmann Date: Fri, 1 Sep 2023 20:07:28 +0200 Subject: [PATCH 10/20] Decreased React warnings and improved dark mode --- Website/src/app/components/dynlist.tsx | 18 +++++++------- Website/src/app/components/iconSelection.tsx | 17 ++++++------- Website/src/app/list/page.tsx | 2 +- Website/src/app/management/poi/client.tsx | 24 ++++++++++++++++--- .../src/app/management/poiTypes/client.tsx | 6 ++--- 5 files changed, 43 insertions(+), 24 deletions(-) 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 27492971..4959a5a8 100644 --- a/Website/src/app/components/iconSelection.tsx +++ b/Website/src/app/components/iconSelection.tsx @@ -3,6 +3,7 @@ import { Option } from "@/utils/types"; import { useMemo } from "react"; import { POIIconCommonName, POIIconImg } from "@/utils/common"; import { POITypeIcon, POITypeIconValues } from "@/utils/api"; +import Image from "next/image" const POI_ICONS: POITypeIcon[] = Object.values(POITypeIconValues); @@ -17,8 +18,8 @@ export default function IconSelection({ id, name }: { - currentIcon: POITypeIcon | null; - setIcon: (newIcon: POITypeIcon | null) => void; + currentIcon: POITypeIcon | ""; + setIcon: (newIcon: POITypeIcon | "") => void; setModified?: (modified: boolean) => void; className?: string; id: string; @@ -31,7 +32,7 @@ export default function IconSelection({ label: (
- {POIIconCommonName[i]} +
{POIIconCommonName[i]}
@@ -39,9 +40,9 @@ export default function IconSelection({ })), [] ); - const defaultIcon: Option = useMemo( + const defaultIcon: Option<""> = useMemo( () => ({ - value: null, + value: "", label: (
[Bitte auswählen] @@ -51,14 +52,14 @@ export default function IconSelection({ [] ); - const icon: Option = 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 && newValue.value !== null) { + function changeFunction(newValue: SingleValue>) { + if (newValue && newValue.value !== "") { setIcon(newValue.value); setModified ? setModified(true) : undefined; } 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 }); @@ -186,14 +186,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()}