diff --git a/server/src/trains/dto/create-train.dto.ts b/server/src/trains/dto/create-train.dto.ts index 51362b0..4a1ced3 100644 --- a/server/src/trains/dto/create-train.dto.ts +++ b/server/src/trains/dto/create-train.dto.ts @@ -1,10 +1,10 @@ import { ApiProperty } from "@nestjs/swagger" import { JsonValue } from "@prisma/client/runtime/library" import { Type } from "class-transformer" -import { IsDate, IsInt, IsJSON, IsNumber, IsString } from "class-validator" +import { IsDate, IsInt, IsJSON, IsNumber, IsString, IsUUID } from "class-validator" export class CreateTrainDto { - @IsString() + @IsUUID() @ApiProperty({ description: "Identifiant du train, donné par l'identifiant de partage Interrail" }) id: string diff --git a/server/src/trains/dto/import-train.dto.ts b/server/src/trains/dto/import-train.dto.ts new file mode 100644 index 0000000..0157f10 --- /dev/null +++ b/server/src/trains/dto/import-train.dto.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from "@nestjs/swagger" +import { IsUUID } from "class-validator" + +export class ImportTrainDto { + @IsUUID() + @ApiProperty({ description: "Identifiant de partage Interrail" }) + id: string +} diff --git a/server/src/trains/dto/interrail-api.dto.ts b/server/src/trains/dto/interrail-api.dto.ts new file mode 100644 index 0000000..ce74cc3 --- /dev/null +++ b/server/src/trains/dto/interrail-api.dto.ts @@ -0,0 +1,75 @@ +export interface InterrailLeg { + infoJson: string + sortOrder: number +} + +export interface InterrailTravel { + date: string + infoJson: string + from: string + to: string + type: number + legs: InterrailLeg[] +} + +export interface InterrailJourneyData { + travels: InterrailTravel[] +} + +export interface InterrailJourney { + data: InterrailJourneyData +} + +export interface InterrailTime { + hours: number + minutes: number + offset: number +} + +export interface InterrailDate { + day: number + month: number + year: number +} + +export interface InterrailTravelInfo { + arrivalTime: InterrailTime + date: InterrailDate + departureTime: InterrailTime + haconVersion: number + dataSource: number +} + +export interface InterrailStopExtraInfo { + departureTime: InterrailTime + index: number +} + +export interface InterrailStopCoordinates { + latitude: number + longitude: number +} + +export interface InterrailStopStation { + coordinates: InterrailStopCoordinates + country: string + name: string + stationId: number +} + +export interface InterrailLegInfo { + attributeCodes: string[] + attributes: object + duration: InterrailTime + directionStation: string + endTime: InterrailTime + isSeparateTicket: boolean + operationDays: string + operator: object + dataSource: number + startTime: InterrailTime + stopExtraInfo: InterrailStopExtraInfo[] + trainName: string + trainStopStations: InterrailStopStation[] + trainType: number +} diff --git a/server/src/trains/dto/osmr-api.dto.ts b/server/src/trains/dto/osmr-api.dto.ts new file mode 100644 index 0000000..799cdc8 --- /dev/null +++ b/server/src/trains/dto/osmr-api.dto.ts @@ -0,0 +1,71 @@ +export enum DrivingSide { + LEFT = "left", + RIGHT = "right", +} + +export type OSMRLocation = number[] + +export interface OSMRWaypoint { + location: OSMRLocation + distance: number + hint: string + name: string +} + +export interface OSMRIntersection { + location: OSMRLocation + bearings: number[] + entry: boolean[] + in?: number + out?: number + classes: string[] +} + +export interface OSMRManeuver { + type: string + location: OSMRLocation + bearing_before: number + bearing_after: number +} + +export interface OSMRLegStep { + distance: number + duration: number + weight: number + driving_side: DrivingSide + intersections: OSMRIntersection[] + ref: string + name: string + mode: string + maneuver: OSMRManeuver + geometry: string +} + +export interface OSMRLeg { + distance: number + steps: OSMRLegStep[] + duration: number + weight: number + summary: string +} + +export interface OSMRRoute { + distance: number + weight_name: SVGStringList + legs: OSMRLeg[] + duration: number + weight: number + geometry: string +} + +export interface OSMRTrain { + waypoints: OSMRWaypoint[] + routes: OSMRRoute[] +} + +export interface OSMRError { + code: string + message: string +} + +export type OSMRResponse = OSMRTrain & OSMRError diff --git a/server/src/trains/trains.controller.ts b/server/src/trains/trains.controller.ts index 0c5a91a..f4e214a 100644 --- a/server/src/trains/trains.controller.ts +++ b/server/src/trains/trains.controller.ts @@ -1,13 +1,14 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, HttpCode, UseGuards, Query, ParseIntPipe, NotFoundException } from '@nestjs/common' +import { Controller, Get, Post, Body, Patch, Param, Delete, HttpCode, UseGuards, Query, ParseIntPipe, NotFoundException, Req } from '@nestjs/common' import { TrainsService } from './trains.service' import { CreateTrainDto } from './dto/create-train.dto' import { UpdateTrainDto } from './dto/update-train.dto' import { TrainEntity } from './entities/train.entity' -import { JwtAuthGuard } from 'src/auth/jwt-auth.guard' +import { AuthenticatedRequest, JwtAuthGuard } from 'src/auth/jwt-auth.guard' import { ApiBearerAuth, ApiCreatedResponse, ApiForbiddenResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger' import { ApiOkResponsePaginated, paginateOutput } from 'src/common/utils/pagination.utils' import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' import { PaginateOutputDto } from 'src/common/dto/pagination-output.dto' +import { ImportTrainDto } from './dto/import-train.dto' @Controller('trains') export class TrainsController { @@ -72,4 +73,16 @@ export class TrainsController { async remove(@Param('id') id: string) { await this.trainsService.remove(id) } + + @Post("/import") + @HttpCode(201) + @UseGuards(JwtAuthGuard) + @ApiBearerAuth() + @ApiCreatedResponse({ type: TrainEntity, description: "Train importé avec succès" }) + @ApiUnauthorizedResponse({ description: "Non authentifié⋅e" }) + @ApiForbiddenResponse({ description: "Permission refusée" }) + async import(@Req() request: AuthenticatedRequest, @Body() importTrainDto: ImportTrainDto): Promise { + const train = await this.trainsService.import(request.user, importTrainDto) + return new TrainEntity(train) + } } diff --git a/server/src/trains/trains.service.ts b/server/src/trains/trains.service.ts index b872399..71f6ca1 100644 --- a/server/src/trains/trains.service.ts +++ b/server/src/trains/trains.service.ts @@ -1,10 +1,14 @@ -import { Injectable } from '@nestjs/common' +import { Injectable, NotAcceptableException } from '@nestjs/common' import { CreateTrainDto } from './dto/create-train.dto' import { UpdateTrainDto } from './dto/update-train.dto' import { PrismaService } from 'src/prisma/prisma.service' -import { TrainTrip } from '@prisma/client' +import { TrainTrip, User } from '@prisma/client' import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' import { paginate } from 'src/common/utils/pagination.utils' +import { ImportTrainDto } from './dto/import-train.dto' +import { InterrailJourney, InterrailLegInfo, InterrailTravelInfo } from './dto/interrail-api.dto' +import { JsonObject } from '@prisma/client/runtime/library' +import { OSMRResponse } from './dto/osmr-api.dto' @Injectable() export class TrainsService { @@ -19,7 +23,7 @@ export class TrainsService { await this.prisma.trainTrip.findMany({ ...paginate(queryPagination), }), - await this.prisma.challenge.count(), + await this.prisma.trainTrip.count(), ] } @@ -41,4 +45,61 @@ export class TrainsService { where: { id }, }) } + + async import(user: User, { id: trainId }: ImportTrainDto): Promise { + const interrailResult: InterrailJourney = await fetch(`https://3uiwjsimnh.execute-api.eu-central-1.amazonaws.com/Prod/journey-import?id=${trainId}`) + .then(data => data.json()) + if (interrailResult.data.travels.length !== 1) + throw new NotAcceptableException(`Ce voyage contient ${interrailResult.data.travels.length} trajets. Merci d'ajouter les trajets un à un.`) + const travel = interrailResult.data.travels[0] + if (travel.legs.length !== 1) + throw new NotAcceptableException(`Ce trajet contient ${travel.legs.length} trains. Merci d'ajouter les trajets un à un.`) + const leg = travel.legs[0] + + const travelInfoJson: InterrailTravelInfo = JSON.parse(travel.infoJson) + const departure = new Date(`${travelInfoJson.date.year}-${travelInfoJson.date.month.toString().padStart(2, "0")}-${travelInfoJson.date.day.toString().padStart(2, "0")}` + + `T${travelInfoJson.departureTime.hours.toString().padStart(2, "0")}:${travelInfoJson.departureTime.minutes.toString().padStart(2, "0")}:00+0100`) + departure.setDate(departure.getDate() + travelInfoJson.departureTime.offset) + const arrival = new Date(`${travelInfoJson.date.year}-${travelInfoJson.date.month.toString().padStart(2, "0")}-${travelInfoJson.date.day.toString().padStart(2, "0")}` + + `T${travelInfoJson.arrivalTime.hours.toString().padStart(2, "0")}:${travelInfoJson.arrivalTime.minutes.toString().padStart(2, "0")}:00+0100`) + arrival.setDate(arrival.getDate() + travelInfoJson.arrivalTime.offset) + + const legInfoJson: InterrailLegInfo = JSON.parse(leg.infoJson) + const coordinatesString = legInfoJson.trainStopStations.map(trainStopStation => { + return `${trainStopStation.coordinates.longitude},${trainStopStation.coordinates.latitude}` + }).join(';') + const osmrResult: OSMRResponse = await fetch(`https://signal.eu.org/osm/eu/route/v1/train/${coordinatesString}?overview=full&steps=true&exclude=highspeed`).then(result => result.json()) + if (osmrResult?.code === "NoRoute") + throw new NotAcceptableException("Aucune route n'a été trouvée avec https://signal.eu.org/osm/") + const route = osmrResult.routes[0] + const distance = route.distance + const geometry = route.geometry + + return this.prisma.trainTrip.upsert({ + where: { + id: trainId, + }, + create: { + id: trainId, + userId: user.id, + distance: distance, + from: travel.from, + to: travel.to, + departureTime: departure, + arrivalTime: arrival, + infoJson: leg.infoJson, + geometry: geometry, + }, + update: { + userId: user.id, + distance: distance, + from: travel.from, + to: travel.to, + departureTime: departure, + arrivalTime: arrival, + infoJson: leg.infoJson, + geometry: geometry, + } + }) + } }