Loading backend/src/route/dto/create-route-req.ts 0 → 100644 +9 −0 Original line number Diff line number Diff line import { ApiProperty } from '@nestjs/swagger'; export class CreateRouteReq { @ApiProperty({ name: 'start', type: Number }) start: number; @ApiProperty({ name: 'end', type: Number }) end: number; } backend/src/route/entities/route.entity.ts +9 −1 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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; } backend/src/route/route.controller.ts +27 −4 Original line number Diff line number Diff line 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); } } backend/src/route/route.service.ts +40 −23 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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) => { Loading @@ -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; } } backend/src/route/utils/recommendations.ts +64 −9 Original line number Diff line number Diff line 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<number>(); for (const place of places) { for (const category of place.categories) { Loading @@ -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] = []; Loading @@ -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); Loading @@ -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); Loading @@ -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; } } Loading
backend/src/route/dto/create-route-req.ts 0 → 100644 +9 −0 Original line number Diff line number Diff line import { ApiProperty } from '@nestjs/swagger'; export class CreateRouteReq { @ApiProperty({ name: 'start', type: Number }) start: number; @ApiProperty({ name: 'end', type: Number }) end: number; }
backend/src/route/entities/route.entity.ts +9 −1 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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; }
backend/src/route/route.controller.ts +27 −4 Original line number Diff line number Diff line 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); } }
backend/src/route/route.service.ts +40 −23 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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) => { Loading @@ -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; } }
backend/src/route/utils/recommendations.ts +64 −9 Original line number Diff line number Diff line 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<number>(); for (const place of places) { for (const category of place.categories) { Loading @@ -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] = []; Loading @@ -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); Loading @@ -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); Loading @@ -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; } }