diff --git a/backend/src/route/dto/create-route-req.ts b/backend/src/route/dto/create-route-req.ts new file mode 100644 index 0000000000000000000000000000000000000000..8e515934aa7c98c298e75fe55ff06de4e9a0a3b0 --- /dev/null +++ b/backend/src/route/dto/create-route-req.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateRouteReq { + @ApiProperty({ name: 'start', type: Number }) + start: number; + + @ApiProperty({ name: 'end', type: Number }) + end: number; +} diff --git a/backend/src/route/entities/route.entity.ts b/backend/src/route/entities/route.entity.ts index 764c18ff0c4effaca3f49419b2eef317542ba27a..30f9a975a811357c9b4ec29d07c66574b000629f 100644 --- a/backend/src/route/entities/route.entity.ts +++ b/backend/src/route/entities/route.entity.ts @@ -2,6 +2,11 @@ import { Town } from 'src/town/entities/town.entity'; import { TravelPlace } from 'src/travel-place/entities/travel-place.entity'; import { User } from 'src/user/entities/user.entity'; import { PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, Entity, OneToMany } from 'typeorm'; +enum RouteStatus { + PENDING = 'pending', + ACCEPTED = 'accepted', + REJECTED = 'rejected', +} @Entity() export class Route { @@ -16,11 +21,14 @@ export class Route { @ManyToOne(() => Town, (town) => town.townId, { nullable: false }) town: Town; - @OneToMany(() => TravelPlace, (travelPlace) => travelPlace.route) + @OneToMany(() => TravelPlace, (travelPlace) => travelPlace.route, { eager: true }) travelPlace: TravelPlace[]; @Column({ nullable: false }) startDate: Date; @Column({ nullable: false }) endDate: Date; + + @Column({ nullable: false, default: RouteStatus.PENDING }) + status: RouteStatus; } diff --git a/backend/src/route/route.controller.ts b/backend/src/route/route.controller.ts index 0b80465c54f0797a06f8de471007f596c023bff2..08591cb860c0ec0a4a6b893ecdf309aa518c14d2 100644 --- a/backend/src/route/route.controller.ts +++ b/backend/src/route/route.controller.ts @@ -1,22 +1,45 @@ -import { Controller, Get, Param, UseGuards, Req, Query } from '@nestjs/common'; +import { Controller, Param, UseGuards, Req, Query, Body, Post, Get } from '@nestjs/common'; import { RouteService } from './route.service'; -import { ApiBearerAuth, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { ApiBearerAuth, ApiBody, ApiParam, ApiQuery, ApiTags } from '@nestjs/swagger'; import { LANGUAGES } from 'src/shared/enum/languages.enum'; import { AuthUserGuard } from 'src/auth/user/authUser.guard'; import { CustomUserRequest } from 'src/auth/user/interface/customUserReq'; +import { CreateRouteReq } from './dto/create-route-req'; @Controller('route') @ApiTags('route') export class RouteController { constructor(private readonly routeService: RouteService) {} + @Post('recommend/:idTown') + @ApiQuery({ name: 'lang', type: String }) + @ApiParam({ name: 'idTown', type: Number }) + @ApiBody({ type: CreateRouteReq }) + @ApiBearerAuth('jwt') + @UseGuards(AuthUserGuard) + async recommendRoute( + @Req() req: CustomUserRequest, + @Query('lang') lang: string, + @Param('idTown') idTown: number, + @Body() createRouteReq: CreateRouteReq, + ) { + const { email } = req.user; + return await this.routeService.recommend( + idTown, + email, + lang as LANGUAGES, + createRouteReq.start, + createRouteReq.end, + ); + } + @Get('recommend/:idTown') @ApiQuery({ name: 'lang', type: String }) @ApiParam({ name: 'idTown', type: Number }) @ApiBearerAuth('jwt') @UseGuards(AuthUserGuard) - async recommendRoute(@Req() req: CustomUserRequest, @Query('lang') lang: string, @Param('idTown') idTown: number) { + async recommendRouteGet(@Req() req: CustomUserRequest, @Query('lang') lang: string, @Param('idTown') idTown: number) { const { email } = req.user; - return await this.routeService.recommend(idTown, email, lang as LANGUAGES); + return await this.routeService.getRouteAndPlacesByUser(email, lang as LANGUAGES, idTown); } } diff --git a/backend/src/route/route.service.ts b/backend/src/route/route.service.ts index 1b2cd86a6870924e56628518e0cf352204392233..66a73c413084b201e6ea622b61ea7dae48397f59 100644 --- a/backend/src/route/route.service.ts +++ b/backend/src/route/route.service.ts @@ -12,6 +12,8 @@ import { VisitedService } from 'src/visited/visited.service'; import { GetPlaceDto } from 'src/place/dto/get-place.dto'; import { Visited } from 'src/visited/entities/visited.entity'; import { RecommendPlace } from './dto/recommend-route.dto'; +import { CreateRouteDto } from './dto/create-route.dto'; +import { CreateTravelPlaceDto } from 'src/travel-place/dto/create-travel-place.dto'; @Injectable() export class RouteService { @@ -24,22 +26,11 @@ export class RouteService { private readonly placeService: PlaceService, private readonly visitedService: VisitedService, ) {} - private async createRoute(email: string, idTown: number, idPlace: number, startDate: Date, endDate: Date) { - const user: User = await this.userRepository.findOneBy({ email }); - const town: Town = await this.townRepository.findOneBy({ townId: idTown }); - // const idRoute = await this.routeRepository.save({ user, town, startDate, endDate }); - const idRoute = (await this.routeRepository.save({ user, town, startDate, endDate })).idRoute; - await this.travelPlaceService.create({ - idRoute, - idPlace, - startDate: new Date(), - endDate: new Date(), - done: false, - }); - } - async recommend(idTown: number, email: string, language: LANGUAGES) { + async recommend(idTown: number, email: string, language: LANGUAGES, start, end) { // Obtener los visitados y los candidatos + const town: Town = await this.townRepository.findOneBy({ townId: idTown }); + const user: User = await this.userRepository.findOneBy({ email }); const placesNotVisited: GetPlaceDto[] = await this.placeService.findPlacesNotVisitedByUser(email, language, idTown); const visited: Visited[] = await this.visitedService.getVisitedByUser(email); const placesMapped: RecommendPlace[] = placesNotVisited.map((place) => { @@ -62,16 +53,42 @@ export class RouteService { }; }); - const system = new RecommendationsSystem(); - const chosen: [number, number][] = system.recommend(visitedMapped, placesMapped); - const placesChooen: GetPlaceDto[] = []; - for (const [index] of chosen) { - placesChooen.push(await this.placeService.findOneAndTradAndAvailable(index, LANGUAGES.EN)); - } + const system = new RecommendationsSystem(visitedMapped, placesMapped, start, end); + const chosen: RecommendPlace[] = system.recommend(); + + const startDate: Date = new Date(); + startDate.setHours(start); + const endDate = new Date(); + endDate.setHours(end); - for (const place of placesChooen) { - await this.createRoute(email, idTown, place.idPlace, new Date(), new Date()); + const createRouteDto: CreateRouteDto = { startDate, endDate, town, user }; + const idRoute = (await this.routeRepository.save(createRouteDto)).idRoute; + + for (const curRecommended of chosen) { + const place = await this.placeService.findOneAndTradAndAvailable(curRecommended.idPlace, language); + const endDate = new Date(); + endDate.setHours(curRecommended.closeAt); + const startDate = new Date(); + startDate.setHours(curRecommended.openAt); + + const createTravelPlace: CreateTravelPlaceDto = { + ...place, + done: false, + idRoute, + endDate, + startDate, + }; + + await this.travelPlaceService.create(createTravelPlace); } - return placesChooen; + } + + async getRouteAndPlacesByUser(email: string, language: LANGUAGES, idTown: number) { + const res = await this.routeRepository.findOne({ + relations: ['travelPlace'], + where: { user: { email }, town: { townId: idTown } }, + }); + + return res; } } diff --git a/backend/src/route/utils/recommendations.ts b/backend/src/route/utils/recommendations.ts index 88ee597c3583d4c3c41b0d094ffe91ca51859831..8fb0aa2e3688283730d38cc0c3e88ffcdc1a3ec8 100644 --- a/backend/src/route/utils/recommendations.ts +++ b/backend/src/route/utils/recommendations.ts @@ -1,9 +1,21 @@ import { RecommendPlace } from '../dto/recommend-route.dto'; import { DataFrame, Series } from './math'; -import { customSort } from './sort'; +import { customSort, sortByClose } from './sort'; export class RecommendationsSystem { - getCategories(places: RecommendPlace[]): number[] { + private visited: RecommendPlace[] = []; + private candidates: RecommendPlace[] = []; + private start: number = 0; + private end: number = 0; + + constructor(visited: RecommendPlace[], candidates: RecommendPlace[], start: number, end: number) { + this.visited = visited; + this.candidates = candidates; + this.start = start; + this.end = end; + } + + private getCategories(places: RecommendPlace[]): number[] { const categories = new Set(); for (const place of places) { for (const category of place.categories) { @@ -13,7 +25,7 @@ export class RecommendationsSystem { return Array.from(categories); } - oneHotEncode(categories: number[], placesToEncode: RecommendPlace[]): DataFrame { + private oneHotEncode(categories: number[], placesToEncode: RecommendPlace[]): DataFrame { const data: DataFrame = {}; for (const category of categories) { data[category] = []; @@ -24,7 +36,7 @@ export class RecommendationsSystem { return data; } - rankVisited(visited: RecommendPlace[]): Series { + private rankVisited(visited: RecommendPlace[]): Series { const visitedCategories = this.getCategories(visited); const visitedEncoded = this.oneHotEncode(visitedCategories, visited); const grades = visited.map((place) => place.rating); @@ -44,7 +56,7 @@ export class RecommendationsSystem { return result; } - rankCandidates(candidates: RecommendPlace[], visited: Series): [number, number][] { + private rankCandidates(candidates: RecommendPlace[], visited: Series): [number, number][] { const visitedCategories = Object.keys(visited).map(Number); const candidatesEncoded = this.oneHotEncode(visitedCategories, candidates); @@ -62,9 +74,52 @@ export class RecommendationsSystem { return ranked; } - recommend(visited: RecommendPlace[], candidates: RecommendPlace[]) { - const visitedRanking = this.rankVisited(visited); - const candidatesRanked = this.rankCandidates(candidates, visitedRanking); - return candidatesRanked; + takeTopNValid(recommendations: [number, number][]): RecommendPlace[] { + const validRecommendations: RecommendPlace[] = []; + + // Filtrar los lugares que existen en candidates + for (const [id] of recommendations) { + const place = this.candidates.find((candidate) => candidate.idPlace === id); + if (place) { + validRecommendations.push(place); + } + } + + validRecommendations.sort(sortByClose); + + const chosen: RecommendPlace[] = []; + let currentTime = this.start; // Tiempo inicial de la ruta + + for (const place of validRecommendations) { + // Verificar si el lugar está abierto y si se puede acomodar dentro del horario + const maxStart = Math.max(currentTime, place.openAt); + const minEnd = Math.min((currentTime + 2) % 24, place.closeAt); + + if (minEnd - 2 >= maxStart) { + // Verificar si aún hay tiempo suficiente en el rango total de la ruta + if ((currentTime + 2) % 24 <= this.end) { + // Actualizar el lugar con el nuevo horario dentro de la ruta + place.openAt = Math.max(currentTime, place.openAt); + place.closeAt = (place.openAt + 2) % 24; + + chosen.push(place); + + // Actualizar la hora actual para el siguiente lugar + currentTime = (currentTime + 2) % 24; + } else { + // Si ya no hay tiempo suficiente para otro lugar, terminamos el bucle + break; + } + } + } + + return chosen; + } + + recommend(): RecommendPlace[] { + const visitedRanking = this.rankVisited(this.visited); + const candidatesRanked = this.rankCandidates(this.candidates, visitedRanking); + const chosen = this.takeTopNValid(candidatesRanked); + return chosen; } } diff --git a/backend/src/route/utils/sort.ts b/backend/src/route/utils/sort.ts index 4da8984a3f7d64ac61fdc37aebe6ec1178b72ff3..042907e897469de16bea6bd00b3da279bbbacb15 100644 --- a/backend/src/route/utils/sort.ts +++ b/backend/src/route/utils/sort.ts @@ -1,3 +1,9 @@ +import { RecommendPlace } from '../dto/recommend-route.dto'; + export function customSort(a: [number, number], b: [number, number]): number { return b[1] - a[1]; } + +export const sortByClose = (a: RecommendPlace, b: RecommendPlace) => { + return a.closeAt - b.closeAt; +}; diff --git a/backend/src/travel-place/entities/travel-place.entity.ts b/backend/src/travel-place/entities/travel-place.entity.ts index 5d095bd4cbade1f8d9144ad7ea4589af72e46619..85e269e471b93dbc21fdb9602fe9fa64538946ec 100644 --- a/backend/src/travel-place/entities/travel-place.entity.ts +++ b/backend/src/travel-place/entities/travel-place.entity.ts @@ -12,7 +12,7 @@ export class TravelPlace { route: Route; @JoinColumn({ name: 'place' }) - @ManyToOne(() => Place, (place) => place.idPlace) + @ManyToOne(() => Place, (place) => place.idPlace, { eager: true }) place: Place; @Column({ nullable: false })