Compare commits
	
		
			2 Commits
		
	
	
		
			e052b06c83
			...
			11ab6f66f7
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 11ab6f66f7 | |||
| 99bd7a88a5 | 
| @@ -0,0 +1,8 @@ | ||||
| /* | ||||
|   Warnings: | ||||
|  | ||||
|   - You are about to drop the column `geometry` on the `TrainTrip` table. All the data in the column will be lost. | ||||
|  | ||||
| */ | ||||
| -- AlterTable | ||||
| ALTER TABLE "TrainTrip" DROP COLUMN "geometry"; | ||||
| @@ -65,7 +65,6 @@ model TrainTrip { | ||||
|   departureTime DateTime     @db.Timestamptz(3) | ||||
|   arrivalTime   DateTime     @db.Timestamptz(3) | ||||
|   infoJson      Json | ||||
|   geometry      String | ||||
|   moneyUpdate   MoneyUpdate? | ||||
| } | ||||
|  | ||||
|   | ||||
							
								
								
									
										23
									
								
								server/src/common/utils/calculus.utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								server/src/common/utils/calculus.utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| const EARTH_RADIUS = 6378137 | ||||
|  | ||||
| type Coordinates = { | ||||
|   latitude: number | ||||
|   longitude: number | ||||
| } | ||||
|  | ||||
| type UseDistanceTypes = { | ||||
|   from: Coordinates | ||||
|   to: Coordinates | ||||
| } | ||||
|  | ||||
| export function toRadians(degrees: number) { | ||||
|   return (degrees * Math.PI) / 180 | ||||
| } | ||||
|  | ||||
| export function distanceCoordinates({ from, to }: UseDistanceTypes) { | ||||
|   const distance = EARTH_RADIUS * Math.acos( | ||||
|     Math.sin(toRadians(to.latitude)) * Math.sin(toRadians(from.latitude)) + | ||||
|       Math.cos(toRadians(to.latitude)) * Math.cos(toRadians(from.latitude)) * Math.cos(toRadians(from.longitude) - toRadians(to.longitude)), | ||||
|   ) | ||||
|   return distance | ||||
| } | ||||
| @@ -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 | ||||
|  | ||||
| @@ -15,7 +15,7 @@ export class CreateTrainDto { | ||||
|  | ||||
|   @IsNumber() | ||||
|   @Type(() => Number) | ||||
|   @ApiProperty({ description: "Distance en mètres du trajet, calculé sur https://signal.eu.org/osm/" }) | ||||
|   @ApiProperty({ description: "Distance estimée en mètres du trajet" }) | ||||
|   distance: number | ||||
|  | ||||
|   @IsString() | ||||
| @@ -39,8 +39,4 @@ export class CreateTrainDto { | ||||
|   @IsJSON() | ||||
|   @ApiProperty({ description: "Informations JSON supplémentaires transmises par Interrail" }) | ||||
|   infoJson: JsonValue | ||||
|  | ||||
|   @IsString() | ||||
|   @ApiProperty({ description: "Géométrie de la course, obtenue par https://signal.eu.org/osm/" }) | ||||
|   geometry: string | ||||
| } | ||||
|   | ||||
							
								
								
									
										8
									
								
								server/src/trains/dto/import-train.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								server/src/trains/dto/import-train.dto.ts
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										75
									
								
								server/src/trains/dto/interrail-api.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								server/src/trains/dto/interrail-api.dto.ts
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
| @@ -13,7 +13,7 @@ export class TrainEntity implements TrainTrip { | ||||
|   @ApiProperty({ description: "Identifiant de l'utilisateur⋅rice effectuant le trajet" }) | ||||
|   userId: number | ||||
|  | ||||
|   @ApiProperty({ description: "Distance en mètres du trajet, calculé sur https://signal.eu.org/osm/" }) | ||||
|   @ApiProperty({ description: "Distance estimée en mètres du trajet" }) | ||||
|   distance: number | ||||
|  | ||||
|   @ApiProperty({ description: "Nom de la gare de départ" }) | ||||
| @@ -30,7 +30,4 @@ export class TrainEntity implements TrainTrip { | ||||
|  | ||||
|   @ApiProperty({ description: "Informations JSON supplémentaires transmises par Interrail" }) | ||||
|   infoJson: JsonValue | ||||
|  | ||||
|   @ApiProperty({ description: "Géométrie de la course, obtenue par https://signal.eu.org/osm/" }) | ||||
|   geometry: string | ||||
| } | ||||
|   | ||||
| @@ -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<TrainEntity> { | ||||
|     const train = await this.trainsService.import(request.user, importTrainDto) | ||||
|     return new TrainEntity(train) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,13 @@ | ||||
| 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 { distanceCoordinates } from 'src/common/utils/calculus.utils' | ||||
|  | ||||
| @Injectable() | ||||
| export class TrainsService { | ||||
| @@ -19,7 +22,7 @@ export class TrainsService { | ||||
|       await this.prisma.trainTrip.findMany({ | ||||
|         ...paginate(queryPagination), | ||||
|       }), | ||||
|       await this.prisma.challenge.count(), | ||||
|       await this.prisma.trainTrip.count(), | ||||
|     ] | ||||
|   } | ||||
|  | ||||
| @@ -41,4 +44,58 @@ export class TrainsService { | ||||
|       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 distance = legInfoJson.trainStopStations | ||||
|         .map(trainStopStation => trainStopStation.coordinates) | ||||
|         .reduce((distance, coordinates, index) => { | ||||
|           if (index === 0) | ||||
|             return distance | ||||
|           const oldCoordinates = legInfoJson.trainStopStations.at(index - 1).coordinates | ||||
|           return distance + distanceCoordinates({ from: oldCoordinates, to: coordinates }) | ||||
|         }, 0) | ||||
|  | ||||
|     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, | ||||
|       }, | ||||
|       update: { | ||||
|         userId: user.id, | ||||
|         distance: distance, | ||||
|         from: travel.from, | ||||
|         to: travel.to, | ||||
|         departureTime: departure, | ||||
|         arrivalTime: arrival, | ||||
|         infoJson: leg.infoJson, | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user