🚀 Complete Laca City Website with VPS Deployment
- Added complete Next.js frontend with responsive design - Added NestJS backend with PostgreSQL and Redis - Added comprehensive VPS deployment script (vps-deploy.sh) - Added deployment guide and documentation - Added all assets and static files - Configured SSL, Nginx, PM2, and monitoring - Ready for production deployment on any VPS
This commit is contained in:
@@ -4,7 +4,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ThrottlerModule } from '@nestjs/throttler';
|
||||
import { DatabaseConfig } from './config/database.config';
|
||||
import { ParkingModule } from './modules/parking/parking.module';
|
||||
import { RoutingModule } from './modules/routing/routing.module';
|
||||
import { UsersModule } from './modules/users/users.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { HealthModule } from './modules/health/health.module';
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, IsOptional, IsEnum, Min, Max } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class RouteRequestDto {
|
||||
@ApiProperty({
|
||||
description: 'Origin latitude',
|
||||
example: 1.3521,
|
||||
minimum: -90,
|
||||
maximum: 90
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
originLat: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Origin longitude',
|
||||
example: 103.8198,
|
||||
minimum: -180,
|
||||
maximum: 180
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(-180)
|
||||
@Max(180)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
originLng: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Destination latitude',
|
||||
example: 1.3500,
|
||||
minimum: -90,
|
||||
maximum: 90
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
destinationLat: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Destination longitude',
|
||||
example: 103.8150,
|
||||
minimum: -180,
|
||||
maximum: 180
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(-180)
|
||||
@Max(180)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
destinationLng: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Transportation mode',
|
||||
example: 'auto',
|
||||
enum: ['auto', 'bicycle', 'pedestrian'],
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(['auto', 'bicycle', 'pedestrian'])
|
||||
costing?: 'auto' | 'bicycle' | 'pedestrian' = 'auto';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Number of alternative routes',
|
||||
example: 2,
|
||||
minimum: 0,
|
||||
maximum: 3,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(3)
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
alternatives?: number = 1;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Avoid highways',
|
||||
example: false,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
avoidHighways?: boolean = false;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Avoid tolls',
|
||||
example: false,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
avoidTolls?: boolean = false;
|
||||
}
|
||||
|
||||
export interface RoutePoint {
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
export interface RouteStep {
|
||||
instruction: string;
|
||||
distance: number; // meters
|
||||
time: number; // seconds
|
||||
type: string;
|
||||
geometry: RoutePoint[];
|
||||
}
|
||||
|
||||
export interface Route {
|
||||
summary: {
|
||||
distance: number; // km
|
||||
time: number; // minutes
|
||||
cost?: number; // estimated cost
|
||||
};
|
||||
geometry: RoutePoint[];
|
||||
steps: RouteStep[];
|
||||
confidence: number;
|
||||
}
|
||||
|
||||
export interface RouteResponse {
|
||||
routes: Route[];
|
||||
origin: RoutePoint;
|
||||
destination: RoutePoint;
|
||||
requestId: string;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Body,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { RoutingService } from './routing.service';
|
||||
import { RouteRequestDto, RouteResponse } from './dto/route-request.dto';
|
||||
|
||||
@ApiTags('Routing')
|
||||
@Controller('routing')
|
||||
@UseGuards(ThrottlerGuard)
|
||||
export class RoutingController {
|
||||
constructor(private readonly routingService: RoutingService) {}
|
||||
|
||||
@Post('calculate')
|
||||
@ApiOperation({
|
||||
summary: 'Calculate route between two points',
|
||||
description: 'Generate turn-by-turn directions using Valhalla routing engine'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Successfully calculated route',
|
||||
type: 'object',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Invalid route parameters',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'No route found between the specified locations',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.SERVICE_UNAVAILABLE,
|
||||
description: 'Routing service unavailable',
|
||||
})
|
||||
async calculateRoute(@Body() dto: RouteRequestDto): Promise<RouteResponse> {
|
||||
return this.routingService.calculateRoute(dto);
|
||||
}
|
||||
|
||||
@Get('status')
|
||||
@ApiOperation({
|
||||
summary: 'Check routing service status',
|
||||
description: 'Check if the Valhalla routing service is healthy and responsive'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Service status information',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: { type: 'string', example: 'healthy' },
|
||||
version: { type: 'string', example: '3.1.0' },
|
||||
},
|
||||
},
|
||||
})
|
||||
async getServiceStatus(): Promise<{ status: string; version?: string }> {
|
||||
return this.routingService.getServiceStatus();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { RoutingController } from './routing.controller';
|
||||
import { RoutingService } from './routing.service';
|
||||
|
||||
@Module({
|
||||
controllers: [RoutingController],
|
||||
providers: [RoutingService],
|
||||
exports: [RoutingService],
|
||||
})
|
||||
export class RoutingModule {}
|
||||
@@ -1,232 +0,0 @@
|
||||
import { Injectable, Logger, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import axios, { AxiosInstance } from 'axios';
|
||||
import { RouteRequestDto, RouteResponse, Route, RoutePoint, RouteStep } from './dto/route-request.dto';
|
||||
|
||||
@Injectable()
|
||||
export class RoutingService {
|
||||
private readonly logger = new Logger(RoutingService.name);
|
||||
private readonly valhallaClient: AxiosInstance;
|
||||
private readonly valhallaUrl: string;
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
this.valhallaUrl = this.configService.get<string>('VALHALLA_URL') || 'http://valhalla:8002';
|
||||
this.valhallaClient = axios.create({
|
||||
baseURL: this.valhallaUrl,
|
||||
timeout: 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async calculateRoute(dto: RouteRequestDto): Promise<RouteResponse> {
|
||||
try {
|
||||
this.logger.debug(`Calculating route from ${dto.originLat},${dto.originLng} to ${dto.destinationLat},${dto.destinationLng}`);
|
||||
|
||||
const requestId = this.generateRequestId();
|
||||
|
||||
const valhallaRequest = this.buildValhallaRequest(dto);
|
||||
|
||||
const response = await this.valhallaClient.post('/route', valhallaRequest);
|
||||
|
||||
if (!response.data || !response.data.trip) {
|
||||
throw new Error('Invalid response from Valhalla routing engine');
|
||||
}
|
||||
|
||||
const routes = this.parseValhallaResponse(response.data);
|
||||
|
||||
return {
|
||||
routes,
|
||||
origin: { lat: dto.originLat, lng: dto.originLng },
|
||||
destination: { lat: dto.destinationLat, lng: dto.destinationLng },
|
||||
requestId,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to calculate route', error);
|
||||
|
||||
if (error.response?.status === 400) {
|
||||
throw new HttpException(
|
||||
'Invalid route request parameters',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
if (error.response?.status === 404) {
|
||||
throw new HttpException(
|
||||
'No route found between the specified locations',
|
||||
HttpStatus.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
throw new HttpException(
|
||||
'Route calculation service unavailable',
|
||||
HttpStatus.SERVICE_UNAVAILABLE,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private buildValhallaRequest(dto: RouteRequestDto) {
|
||||
const locations = [
|
||||
{ lat: dto.originLat, lon: dto.originLng },
|
||||
{ lat: dto.destinationLat, lon: dto.destinationLng },
|
||||
];
|
||||
|
||||
const costingOptions = this.getCostingOptions(dto);
|
||||
|
||||
return {
|
||||
locations,
|
||||
costing: dto.costing,
|
||||
costing_options: costingOptions,
|
||||
directions_options: {
|
||||
units: 'kilometers',
|
||||
language: 'en-US',
|
||||
narrative: true,
|
||||
alternates: dto.alternatives || 1,
|
||||
},
|
||||
format: 'json',
|
||||
shape_match: 'edge_walk',
|
||||
encoded_polyline: true,
|
||||
};
|
||||
}
|
||||
|
||||
private getCostingOptions(dto: RouteRequestDto) {
|
||||
const options: any = {};
|
||||
|
||||
if (dto.costing === 'auto') {
|
||||
options.auto = {
|
||||
maneuver_penalty: 5,
|
||||
gate_cost: 30,
|
||||
gate_penalty: 300,
|
||||
private_access_penalty: 450,
|
||||
toll_booth_cost: 15,
|
||||
toll_booth_penalty: 0,
|
||||
ferry_cost: 300,
|
||||
use_ferry: dto.avoidTolls ? 0 : 1,
|
||||
use_highways: dto.avoidHighways ? 0 : 1,
|
||||
use_tolls: dto.avoidTolls ? 0 : 1,
|
||||
};
|
||||
} else if (dto.costing === 'bicycle') {
|
||||
options.bicycle = {
|
||||
maneuver_penalty: 5,
|
||||
gate_penalty: 300,
|
||||
use_roads: 0.5,
|
||||
use_hills: 0.5,
|
||||
use_ferry: 1,
|
||||
avoid_bad_surfaces: 0.25,
|
||||
};
|
||||
} else if (dto.costing === 'pedestrian') {
|
||||
options.pedestrian = {
|
||||
walking_speed: 5.1,
|
||||
walkway_factor: 1,
|
||||
sidewalk_factor: 1,
|
||||
alley_factor: 2,
|
||||
driveway_factor: 5,
|
||||
step_penalty: 0,
|
||||
use_ferry: 1,
|
||||
use_living_streets: 0.6,
|
||||
};
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
private parseValhallaResponse(data: any): Route[] {
|
||||
const trip = data.trip;
|
||||
|
||||
if (!trip || !trip.legs || trip.legs.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const route: Route = {
|
||||
summary: {
|
||||
distance: Math.round(trip.summary.length * 100) / 100, // km
|
||||
time: Math.round(trip.summary.time / 60 * 100) / 100, // minutes
|
||||
cost: this.estimateFuelCost(trip.summary.length, 'auto'),
|
||||
},
|
||||
geometry: this.decodePolyline(trip.shape),
|
||||
steps: this.parseManeuvers(trip.legs[0].maneuvers),
|
||||
confidence: 0.95,
|
||||
};
|
||||
|
||||
return [route];
|
||||
}
|
||||
|
||||
private parseManeuvers(maneuvers: any[]): RouteStep[] {
|
||||
return maneuvers.map(maneuver => ({
|
||||
instruction: maneuver.instruction,
|
||||
distance: Math.round(maneuver.length * 1000), // convert km to meters
|
||||
time: maneuver.time, // seconds
|
||||
type: maneuver.type?.toString() || 'unknown',
|
||||
geometry: [], // Would need additional processing for step-by-step geometry
|
||||
}));
|
||||
}
|
||||
|
||||
private decodePolyline(encoded: string): RoutePoint[] {
|
||||
// Simplified polyline decoding - in production, use a proper polyline library
|
||||
const points: RoutePoint[] = [];
|
||||
let index = 0;
|
||||
let lat = 0;
|
||||
let lng = 0;
|
||||
|
||||
while (index < encoded.length) {
|
||||
let result = 1;
|
||||
let shift = 0;
|
||||
let b: number;
|
||||
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63 - 1;
|
||||
result += b << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x1f);
|
||||
|
||||
lat += (result & 1) !== 0 ? ~(result >> 1) : (result >> 1);
|
||||
|
||||
result = 1;
|
||||
shift = 0;
|
||||
|
||||
do {
|
||||
b = encoded.charCodeAt(index++) - 63 - 1;
|
||||
result += b << shift;
|
||||
shift += 5;
|
||||
} while (b >= 0x1f);
|
||||
|
||||
lng += (result & 1) !== 0 ? ~(result >> 1) : (result >> 1);
|
||||
|
||||
points.push({
|
||||
lat: lat / 1e5,
|
||||
lng: lng / 1e5,
|
||||
});
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
private estimateFuelCost(distanceKm: number, costing: string): number {
|
||||
if (costing !== 'auto') return 0;
|
||||
|
||||
const fuelEfficiency = 10; // km per liter
|
||||
const fuelPricePerLiter = 1.5; // USD
|
||||
|
||||
return Math.round((distanceKm / fuelEfficiency) * fuelPricePerLiter * 100) / 100;
|
||||
}
|
||||
|
||||
private generateRequestId(): string {
|
||||
return `route_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
async getServiceStatus(): Promise<{ status: string; version?: string }> {
|
||||
try {
|
||||
const response = await this.valhallaClient.get('/status');
|
||||
return {
|
||||
status: 'healthy',
|
||||
version: response.data?.version,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Valhalla service health check failed', error);
|
||||
return {
|
||||
status: 'unhealthy',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user