Skip to content

Commit

Permalink
Merge pull request #16 from moevm/routes
Browse files Browse the repository at this point in the history
Routes module
  • Loading branch information
jonx8 authored Jan 10, 2025
2 parents 51d8ae9 + c5eba07 commit 8e2c8c0
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 60 deletions.
2 changes: 2 additions & 0 deletions server/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {Neo4jConfig} from './modules/neo4j/neo4j-config.interface';
import {PointsOfInterestModule} from './modules/points-of-interest/points-of-interest.module';
import {UsersModule} from "./modules/users/users.module";
import {AuthorizationModule} from "./modules/authorization/authorization.module";
import { RoutesModule } from './modules/routes/routes.module';

@Module({
imports: [
Expand All @@ -25,6 +26,7 @@ import {AuthorizationModule} from "./modules/authorization/authorization.module"
PointsOfInterestModule,
UsersModule,
AuthorizationModule,
RoutesModule
],
controllers: [AppController],
providers: [],
Expand Down
13 changes: 12 additions & 1 deletion server/src/modules/neo4j/neo4j-error.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,26 @@ export class Neo4jErrorFilter implements ExceptionFilter {

let statusCode = 500;
let error = 'Internal Server Error';
let message: string[] = undefined;
let message: string[] = [exception.message];

// Neo.ClientError.Schema.ConstraintValidationFailed
// Node already exists with label `...` and property `...`'
if (exception.message.includes('already exists with')) {
const [_, property] = exception.message.match(/`([a-z0-9]+)`/gi);
message = [`${property.replace(/`/g, '')} already taken`];
statusCode = 400;
error = 'Bad Request';
}

// Neo.ClientError.Procedure.ProcedureCallFailed
// Failed to invoke procedure `apoc.util.validate`: Caused by: java.lang.RuntimeException: Point of interest with id ... not found
if (exception.message.includes('not found')) {
const messagePosition = exception.message.lastIndexOf(': ')
message = [exception.message.substring(messagePosition + 2)];
statusCode = 404;
error = 'Not Found';
}

response
.status(statusCode)
.json({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Injectable, NotFoundException} from "@nestjs/common";
import {CreatePointOfInterestDto} from "./dto/create-point-of-interest.dto";
import {Neo4jService} from "../neo4j/neo4j.service";
import { Injectable, NotFoundException } from "@nestjs/common";
import { CreatePointOfInterestDto } from "./dto/create-point-of-interest.dto";
import { Neo4jService } from "../neo4j/neo4j.service";
import PointOfInterest from "./entities/point-of-interest.entity";


Expand Down Expand Up @@ -29,7 +29,8 @@ export class PointsOfInterestService {
location: l,
created_at: Datetime()}
)-[r: CLOSE_TO_THE]->(i)
RETURN poi`);
RETURN poi`
);
const node = result.records.at(0).get('poi');
return new PointOfInterest(
node.elementId,
Expand Down
10 changes: 4 additions & 6 deletions server/src/modules/routes/dto/create-route.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {IsString, IsNumber, MaxLength, MinLength} from "class-validator";
import { IsString, MaxLength, MinLength, IsArray, ArrayMinSize } from "class-validator";

export class CreateRouteDto {
@IsString()
Expand All @@ -10,9 +10,7 @@ export class CreateRouteDto {
@MaxLength(2000)
description: string;

@IsNumber()
length: number;

@IsNumber()
duration: number;
@IsArray()
@ArrayMinSize(2)
points: string[];
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import PointOfInterest from "src/modules/points-of-interest/entities/point-of-interest.entity";

export default class Route {
id: string;
Expand All @@ -6,14 +7,16 @@ export default class Route {
length: number;
duration: number;
createdAt: string;
points: PointOfInterest[]

constructor(id: string, name: string, description: string, length: number, duration: number, createdAt: string) {
constructor(id: string, name: string, description: string, length: number, duration: number, createdAt: string, points: PointOfInterest[]) {
this.id = id;
this.name = name;
this.description = description;
this.length = length;
this.duration = duration;
this.createdAt = createdAt;
this.points = points
}

}
26 changes: 16 additions & 10 deletions server/src/modules/routes/routes.controller.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,34 @@
import {Body, Controller, Get, Param, Post, UsePipes, ValidationPipe} from '@nestjs/common';
import {RoutesService} from './routes.service';
import {CreateRouteDto} from "./dto/create-route.dto";
import {Public} from "../authorization/public.decorator";
import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Req, UsePipes, ValidationPipe } from '@nestjs/common';
import { RoutesService } from './routes.service';
import { Public } from '../authorization/public.decorator';
import { CreateRouteDto } from './dto/create-route.dto';

@Controller("/api/route")
@Controller("/api/routes")
export class RoutesController {
constructor(private readonly RoutesService: RoutesService) {
}

@UsePipes(new ValidationPipe())
@Post()
async create(@Body() createRoutesDto: CreateRouteDto) {
return await this.RoutesService.create(createRoutesDto);
async create(@Body() createRouteDto: CreateRouteDto, @Req() req: any) {
console.log(req.user)
return this.RoutesService.create(createRouteDto, req.user.sub);
}

@Public()
@HttpCode(HttpStatus.OK)
@Post("build")
async build(@Body() poiList: string[]) {
return await this.RoutesService.build(poiList);
}

@Get()
findAll() {
async findAll() {
return this.RoutesService.findAll();
}

@Public()
@Get(':id')
findOne(@Param('id') id: string) {
async findOne(@Param('id') id: string) {
return this.RoutesService.findOne(id);
}
}
155 changes: 118 additions & 37 deletions server/src/modules/routes/routes.service.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,93 @@
import {Injectable, NotFoundException} from "@nestjs/common";
import {CreateRouteDto} from "./dto/create-route.dto";
import {Neo4jService} from "../neo4j/neo4j.service";
import Route from "./entities/routes.entity";
import { Injectable, NotFoundException } from "@nestjs/common";
import { Neo4jService } from "../neo4j/neo4j.service";
import Route from "./entities/route.entity";
import { CreateRouteDto } from "./dto/create-route.dto";
import PointOfInterest from "../points-of-interest/entities/point-of-interest.entity";


@Injectable()
export class RoutesService {
// private static readonly FIND_INTERSECTION_RADIUS: number = 100000;

constructor(private readonly neo4jService: Neo4jService) {
}

async create(dto: CreateRouteDto) {

async create(dto: CreateRouteDto, userId: string) {
const session = this.neo4jService.getWriteSession();
try {
const transaction = session.beginTransaction();
const result = await transaction.run(`
MATCH (user: User WHERE elementId(user) = "${userId}")
CREATE (user)-[:CREATED]->(route :Route {
name: "${dto.name}",
description: "${dto.description}",
created_at: Datetime()
})
WITH route, $poi_list AS points
UNWIND points AS point
OPTIONAL MATCH (poi :PointOfInterest WHERE elementId(poi)=point)
WITH route, poi,
CASE WHEN poi IS NULL THEN TRUE ELSE FALSE END AS node_not_found,
RANGE(0, SIZE(points) - 1) AS order_list, point
CALL apoc.util.validate(node_not_found,'Point of interest with id ' + point + ' not found', [404])
WITH collect(poi) AS points, order_list, route
UNWIND apoc.coll.zip(points, order_list) AS poi
WITH poi[0] AS val, poi[1] AS i, route
CREATE (route)-[:INCLUDE{order: i}]->(val)
RETURN elementId(route) AS Id
LIMIT 1
`,
{ poi_list: dto.points }
);
const routeId = result.records.at(0)?.get('Id');
if (!routeId) {
throw new NotFoundException(`User with id ${userId} not found`)
}
transaction.run(`
MATCH (route: Route WHERE elementId(route) = $routeId)
CALL () {
MATCH
(route)-[inc1: INCLUDE]->
(: PointOfInterest)-[:CLOSE_TO_THE]->(i1: Intersection),
(route)-[inc2: INCLUDE{order: inc1.order + 1}]->
(: PointOfInterest)-[:CLOSE_TO_THE]->(i2: Intersection)
RETURN i1, i2
ORDER BY inc1.order
}
WITH route, i1, i2
CALL apoc.algo.aStarConfig(i1, i2, "ROAD_SEGMENT", {pointPropName: "location", weight: "length"})
YIELD weight
WITH SUM(weight) as total_distance, route
SET route.length = total_distance
SET route.duration = total_distance / 78
RETURN route
`,
{ routeId: routeId }
);
await transaction.commit();
return await this.findOne(routeId);
} finally {
await session.close();
}
}


async build(poiList: string[]) {
const session = this.neo4jService.getWriteSession();
try {
// const result = await session.run(
// `WITH point({latitude: ${dto.location.latitude}, longitude: ${dto.location.longitude}}) AS l
// CALL (l) {
// MATCH (i: Intersection WHERE point.distance(i.location, l) < ${(PointsOfInterestService.FIND_INTERSECTION_RADIUS)})
// RETURN i
// ORDER BY point.distance(i.location, l) ASC
// LIMIT 1
// }
// WITH i, l
// CREATE (poi :Route {
// name: "${dto.name}",
// description: "${dto.description}",
// location: l,
// created_at: Datetime()}
// )-[r: CLOSE_TO_THE]->(i)
// RETURN poi`);
// const node = result.records.at(0).get('poi');
// return new Route(
// node.elementId,
// node.properties.name,
// node.properties.description,
// node.properties.length,
// node.properties.duration,
// node.properties.created_at.toString()
// );
const result = await session.run(
`CALL () {
UNWIND $poi_list AS poi_id
MATCH(poi :PointOfInterest WHERE elementId(poi) = poi_id)-[:CLOSE_TO_THE]->(i :Intersection)
RETURN collect(DISTINCT i) AS i_list
}
WITH i_list[0..-1] AS i1, i_list[1..] AS i2
UNWIND apoc.coll.zip(i1, i2) AS segment
CALL apoc.algo.aStarConfig(segment[0], segment[1], "ROAD_SEGMENT", {pointPropName: "location", weight: "length"})
YIELD weight, path
RETURN weight AS length, path`,
{ poi_list: poiList }
);
return result.records.map(record => record.toObject());
} finally {
await session.close();
}
Expand All @@ -47,18 +96,33 @@ export class RoutesService {
async findAll() {
const session = this.neo4jService.getReadSession();
try {
const result = await session.run(
`MATCH (route :Route) RETURN route`
const result = await session.run(`
MATCH (route :Route)
CALL (route) {
MATCH (route)-[inc :INCLUDE]->(poi :PointOfInterest)
RETURN poi
ORDER BY inc.order
}
RETURN route, collect(poi) AS poi_list
`
)
return result.records.map(record => {
const node = record.get('route');
const poiList: PointOfInterest[] = record.get('poi_list').map(poi => new PointOfInterest(
poi.elementId,
poi.properties.name,
poi.properties.description,
poi.properties.location,
poi.properties.created_at.toString(),
));
return new Route(
node.elementId,
node.properties.name,
node.properties.description,
node.properties.length,
node.properties.duration,
node.properties.created_at.toString()
node.properties.created_at.toString(),
poiList
);
});
} finally {
Expand All @@ -70,9 +134,25 @@ export class RoutesService {
const session = this.neo4jService.getReadSession();
try {
const result = await session.run(
`MATCH (route :Route WHERE elementId(route) = "${id}") RETURN route`
`
MATCH (route :Route WHERE elementId(route) = "${id}")
CALL (route) {
MATCH (route)-[inc :INCLUDE]->(poi :PointOfInterest)
RETURN poi
ORDER BY inc.order
}
RETURN route, collect(poi) AS poi_list
`
)
console.log(result.records)
const node = result.records.at(0)?.get('route');
const poiList: PointOfInterest[] = result.records.at(0)?.get('poi_list').map(poi => new PointOfInterest(
poi.elementId,
poi.properties.name,
poi.properties.description,
poi.properties.location,
poi.properties.created_at.toString(),
));
if (!node) {
throw new NotFoundException(`Route with id: ${id} not found`);
}
Expand All @@ -82,7 +162,8 @@ export class RoutesService {
node.properties.description,
node.properties.length,
node.properties.duration,
node.properties.created_at.toString()
node.properties.created_at.toString(),
poiList
);
} finally {
await session.close();
Expand Down
1 change: 0 additions & 1 deletion server/src/modules/users/users.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import {JwtGuard} from "../authorization/jwt.guard";

@Module({
controllers: [UsersController],
Expand Down

0 comments on commit 8e2c8c0

Please sign in to comment.