Importation des trajets depuis Interrail et signal.eu.org

This commit is contained in:
Emmy D'Anello 2024-12-07 23:57:14 +01:00
parent e052b06c83
commit 99bd7a88a5
Signed by: ynerant
GPG Key ID: 3A75C55819C8CF85
6 changed files with 235 additions and 7 deletions

View File

@ -1,10 +1,10 @@
import { ApiProperty } from "@nestjs/swagger" import { ApiProperty } from "@nestjs/swagger"
import { JsonValue } from "@prisma/client/runtime/library" import { JsonValue } from "@prisma/client/runtime/library"
import { Type } from "class-transformer" 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 { export class CreateTrainDto {
@IsString() @IsUUID()
@ApiProperty({ description: "Identifiant du train, donné par l'identifiant de partage Interrail" }) @ApiProperty({ description: "Identifiant du train, donné par l'identifiant de partage Interrail" })
id: string id: string

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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 { TrainsService } from './trains.service'
import { CreateTrainDto } from './dto/create-train.dto' import { CreateTrainDto } from './dto/create-train.dto'
import { UpdateTrainDto } from './dto/update-train.dto' import { UpdateTrainDto } from './dto/update-train.dto'
import { TrainEntity } from './entities/train.entity' 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 { ApiBearerAuth, ApiCreatedResponse, ApiForbiddenResponse, ApiNotFoundResponse, ApiOkResponse, ApiUnauthorizedResponse } from '@nestjs/swagger'
import { ApiOkResponsePaginated, paginateOutput } from 'src/common/utils/pagination.utils' import { ApiOkResponsePaginated, paginateOutput } from 'src/common/utils/pagination.utils'
import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto' import { QueryPaginationDto } from 'src/common/dto/pagination-query.dto'
import { PaginateOutputDto } from 'src/common/dto/pagination-output.dto' import { PaginateOutputDto } from 'src/common/dto/pagination-output.dto'
import { ImportTrainDto } from './dto/import-train.dto'
@Controller('trains') @Controller('trains')
export class TrainsController { export class TrainsController {
@ -72,4 +73,16 @@ export class TrainsController {
async remove(@Param('id') id: string) { async remove(@Param('id') id: string) {
await this.trainsService.remove(id) 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<TrainEntity> {
const train = await this.trainsService.import(request.user, importTrainDto)
return new TrainEntity(train)
}
} }

View File

@ -1,10 +1,14 @@
import { Injectable } from '@nestjs/common' import { Injectable, NotAcceptableException } from '@nestjs/common'
import { CreateTrainDto } from './dto/create-train.dto' import { CreateTrainDto } from './dto/create-train.dto'
import { UpdateTrainDto } from './dto/update-train.dto' import { UpdateTrainDto } from './dto/update-train.dto'
import { PrismaService } from 'src/prisma/prisma.service' 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 { QueryPaginationDto } from 'src/common/dto/pagination-query.dto'
import { paginate } from 'src/common/utils/pagination.utils' 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() @Injectable()
export class TrainsService { export class TrainsService {
@ -19,7 +23,7 @@ export class TrainsService {
await this.prisma.trainTrip.findMany({ await this.prisma.trainTrip.findMany({
...paginate(queryPagination), ...paginate(queryPagination),
}), }),
await this.prisma.challenge.count(), await this.prisma.trainTrip.count(),
] ]
} }
@ -41,4 +45,61 @@ export class TrainsService {
where: { id }, where: { id },
}) })
} }
async import(user: User, { id: trainId }: ImportTrainDto): Promise<TrainTrip> {
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,
}
})
}
} }