🎯 MapView v2.0 - Global Deployment Ready
✨ MAJOR FEATURES: • Auto-zoom intelligence với smart bounds fitting • Enhanced 3D GPS markers với pulsing effects • Professional route display với 6-layer rendering • Status-based parking icons với availability indicators • Production-ready build optimizations 🗺️ AUTO-ZOOM FEATURES: • Smart bounds fitting cho GPS + selected parking • Adaptive padding (50px) cho visual balance • Max zoom control (level 16) để tránh quá gần • Dynamic centering khi không có selection 🎨 ENHANCED VISUALS: • 3D GPS marker với multi-layer pulse effects • Advanced parking icons với status colors • Selection highlighting với animation • Dimming system cho non-selected items 🛣️ ROUTE SYSTEM: • OpenRouteService API integration • Multi-layer route rendering (glow, shadow, main, animated) • Real-time distance & duration calculation • Visual route info trong popup 📱 PRODUCTION READY: • SSR safe với dynamic imports • Build errors resolved • Global deployment via Vercel • Optimized performance 🌍 DEPLOYMENT: • Vercel: https://whatever-ctk2auuxr-phong12hexdockworks-projects.vercel.app • Bundle size: 22.8 kB optimized • Global CDN distribution • HTTPS enabled 💾 VERSION CONTROL: • MapView-v2.0.tsx backup created • MAPVIEW_VERSIONS.md documentation • Full version history tracking
This commit is contained in:
15
backend/src/modules/auth/auth.controller.ts
Normal file
15
backend/src/modules/auth/auth.controller.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Controller, Post, Body } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@ApiTags('Authentication')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@Post('login')
|
||||
@ApiOperation({ summary: 'User login' })
|
||||
async login(@Body() loginDto: { email: string; password: string }) {
|
||||
return this.authService.login(loginDto);
|
||||
}
|
||||
}
|
||||
10
backend/src/modules/auth/auth.module.ts
Normal file
10
backend/src/modules/auth/auth.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
17
backend/src/modules/auth/auth.service.ts
Normal file
17
backend/src/modules/auth/auth.service.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
async validateUser(email: string, password: string): Promise<any> {
|
||||
// Basic authentication logic placeholder
|
||||
return null;
|
||||
}
|
||||
|
||||
async login(user: any) {
|
||||
// JWT token generation placeholder
|
||||
return {
|
||||
access_token: 'placeholder_token',
|
||||
user,
|
||||
};
|
||||
}
|
||||
}
|
||||
15
backend/src/modules/health/health.controller.ts
Normal file
15
backend/src/modules/health/health.controller.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation } from '@nestjs/swagger';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
@ApiTags('Health')
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(private readonly healthService: HealthService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Health check endpoint' })
|
||||
getHealth() {
|
||||
return this.healthService.getHealth();
|
||||
}
|
||||
}
|
||||
9
backend/src/modules/health/health.module.ts
Normal file
9
backend/src/modules/health/health.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HealthController } from './health.controller';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
providers: [HealthService],
|
||||
})
|
||||
export class HealthModule {}
|
||||
13
backend/src/modules/health/health.service.ts
Normal file
13
backend/src/modules/health/health.service.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class HealthService {
|
||||
getHealth() {
|
||||
return {
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
};
|
||||
}
|
||||
}
|
||||
87
backend/src/modules/parking/dto/find-nearby-parking.dto.ts
Normal file
87
backend/src/modules/parking/dto/find-nearby-parking.dto.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, IsOptional, IsArray, Min, Max } from 'class-validator';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
export class FindNearbyParkingDto {
|
||||
@ApiProperty({
|
||||
description: 'Latitude coordinate',
|
||||
example: 1.3521,
|
||||
minimum: -90,
|
||||
maximum: 90
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
lat: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Longitude coordinate',
|
||||
example: 103.8198,
|
||||
minimum: -180,
|
||||
maximum: 180
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(-180)
|
||||
@Max(180)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
lng: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Search radius in meters',
|
||||
example: 4000,
|
||||
minimum: 100,
|
||||
maximum: 10000,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(100)
|
||||
@Max(10000)
|
||||
@Transform(({ value }) => parseFloat(value))
|
||||
radius?: number = 4000;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Maximum number of results to return',
|
||||
example: 20,
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
maxResults?: number = 20;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Price range filter [min, max] per hour',
|
||||
example: [0, 10],
|
||||
required: false,
|
||||
type: [Number]
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsNumber({}, { each: true })
|
||||
priceRange?: [number, number];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Required amenities',
|
||||
example: ['covered', 'security', 'ev_charging'],
|
||||
required: false,
|
||||
type: [String]
|
||||
})
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
amenities?: string[];
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Filter by availability status',
|
||||
example: 'available',
|
||||
enum: ['available', 'limited', 'full'],
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
availabilityFilter?: 'available' | 'limited' | 'full';
|
||||
}
|
||||
43
backend/src/modules/parking/dto/update-availability.dto.ts
Normal file
43
backend/src/modules/parking/dto/update-availability.dto.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber, IsOptional, IsString, Min, Max } from 'class-validator';
|
||||
|
||||
export class UpdateParkingAvailabilityDto {
|
||||
@ApiProperty({
|
||||
description: 'Number of available parking slots',
|
||||
example: 15,
|
||||
minimum: 0
|
||||
})
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
availableSlots: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Source of the update',
|
||||
example: 'sensor',
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
source?: string = 'manual';
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Confidence level of the update (0-1)',
|
||||
example: 0.95,
|
||||
minimum: 0,
|
||||
maximum: 1,
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(1)
|
||||
confidence?: number = 1.0;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Additional metadata',
|
||||
example: { sensor_id: 'PARK_001', battery_level: 85 },
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { User } from '../../users/entities/user.entity';
|
||||
import { ParkingLot } from './parking-lot.entity';
|
||||
|
||||
@Entity('parking_history')
|
||||
export class ParkingHistory {
|
||||
@ApiProperty({ description: 'Unique identifier for the parking history entry' })
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: 'User who visited the parking lot' })
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({ description: 'Parking lot that was visited' })
|
||||
@Column({ type: 'int' })
|
||||
parkingLotId: number;
|
||||
|
||||
@ApiProperty({ description: 'Date and time of the visit' })
|
||||
@CreateDateColumn()
|
||||
visitDate: Date;
|
||||
|
||||
@ApiProperty({ description: 'Duration of parking in minutes' })
|
||||
@Column({ type: 'int', nullable: true })
|
||||
durationMinutes: number;
|
||||
|
||||
@ApiProperty({ description: 'User rating for the parking experience' })
|
||||
@Column({ type: 'int', nullable: true })
|
||||
rating: number;
|
||||
|
||||
@ApiProperty({ description: 'User review comments' })
|
||||
@Column({ type: 'text', nullable: true })
|
||||
review: string;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => User, (user) => user.parkingHistory, { nullable: true })
|
||||
@JoinColumn({ name: 'userId' })
|
||||
user: User;
|
||||
|
||||
@ManyToOne(() => ParkingLot, (parkingLot) => parkingLot.history)
|
||||
@JoinColumn({ name: 'parkingLotId' })
|
||||
parkingLot: ParkingLot;
|
||||
}
|
||||
121
backend/src/modules/parking/entities/parking-lot.entity.ts
Normal file
121
backend/src/modules/parking/entities/parking-lot.entity.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ParkingHistory } from './parking-history.entity';
|
||||
import { ParkingUpdate } from './parking-update.entity';
|
||||
|
||||
@Entity('parking_lots')
|
||||
export class ParkingLot {
|
||||
@ApiProperty({ description: 'Unique identifier for the parking lot' })
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: 'Name of the parking lot' })
|
||||
@Column({ type: 'varchar', length: 255 })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: 'Address of the parking lot' })
|
||||
@Column({ type: 'text' })
|
||||
address: string;
|
||||
|
||||
@ApiProperty({ description: 'Latitude coordinate' })
|
||||
@Column({ type: 'double precision' })
|
||||
lat: number;
|
||||
|
||||
@ApiProperty({ description: 'Longitude coordinate' })
|
||||
@Column({ type: 'double precision' })
|
||||
lng: number;
|
||||
|
||||
@ApiProperty({ description: 'PostGIS geography point' })
|
||||
@Column({
|
||||
type: 'geography',
|
||||
spatialFeatureType: 'Point',
|
||||
srid: 4326,
|
||||
nullable: true,
|
||||
})
|
||||
location: string;
|
||||
|
||||
@ApiProperty({ description: 'Hourly parking rate' })
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, nullable: true })
|
||||
hourlyRate: number;
|
||||
|
||||
@ApiProperty({ description: 'Opening time' })
|
||||
@Column({ type: 'time', nullable: true })
|
||||
openTime: string;
|
||||
|
||||
@ApiProperty({ description: 'Closing time' })
|
||||
@Column({ type: 'time', nullable: true })
|
||||
closeTime: string;
|
||||
|
||||
@ApiProperty({ description: 'Number of available parking spaces' })
|
||||
@Column({ type: 'int', default: 0 })
|
||||
availableSlots: number;
|
||||
|
||||
@ApiProperty({ description: 'Total number of parking spaces' })
|
||||
@Column({ type: 'int' })
|
||||
totalSlots: number;
|
||||
|
||||
@ApiProperty({ description: 'Parking lot amenities' })
|
||||
@Column({ type: 'jsonb', default: '{}' })
|
||||
amenities: Record<string, any>;
|
||||
|
||||
@ApiProperty({ description: 'Contact information' })
|
||||
@Column({ type: 'jsonb', default: '{}' })
|
||||
contactInfo: Record<string, any>;
|
||||
|
||||
@ApiProperty({ description: 'Whether the parking lot is active' })
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty({ description: 'Creation timestamp' })
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({ description: 'Last update timestamp' })
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
|
||||
// Relations
|
||||
@OneToMany(() => ParkingHistory, (history) => history.parkingLot)
|
||||
history: ParkingHistory[];
|
||||
|
||||
@OneToMany(() => ParkingUpdate, (update) => update.parkingLot)
|
||||
updates: ParkingUpdate[];
|
||||
|
||||
// Computed properties
|
||||
@ApiProperty({ description: 'Occupancy rate as percentage' })
|
||||
get occupancyRate(): number {
|
||||
if (this.totalSlots === 0) return 0;
|
||||
return ((this.totalSlots - this.availableSlots) / this.totalSlots) * 100;
|
||||
}
|
||||
|
||||
@ApiProperty({ description: 'Availability status' })
|
||||
get availabilityStatus(): 'available' | 'limited' | 'full' {
|
||||
const rate = this.occupancyRate;
|
||||
if (rate >= 95) return 'full';
|
||||
if (rate >= 80) return 'limited';
|
||||
return 'available';
|
||||
}
|
||||
|
||||
@ApiProperty({ description: 'Whether the parking lot is currently open' })
|
||||
get isOpen(): boolean {
|
||||
if (!this.openTime || !this.closeTime) return true;
|
||||
|
||||
const now = new Date();
|
||||
const currentTime = now.getHours() * 60 + now.getMinutes();
|
||||
|
||||
const [openHour, openMin] = this.openTime.split(':').map(Number);
|
||||
const [closeHour, closeMin] = this.closeTime.split(':').map(Number);
|
||||
|
||||
const openMinutes = openHour * 60 + openMin;
|
||||
const closeMinutes = closeHour * 60 + closeMin;
|
||||
|
||||
return currentTime >= openMinutes && currentTime <= closeMinutes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ParkingLot } from './parking-lot.entity';
|
||||
|
||||
@Entity('parking_updates')
|
||||
export class ParkingUpdate {
|
||||
@ApiProperty({ description: 'Unique identifier for the parking update' })
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ApiProperty({ description: 'Parking lot being updated' })
|
||||
@Column({ type: 'int' })
|
||||
parkingLotId: number;
|
||||
|
||||
@ApiProperty({ description: 'Number of available slots at time of update' })
|
||||
@Column({ type: 'int' })
|
||||
availableSlots: number;
|
||||
|
||||
@ApiProperty({ description: 'Source of the update' })
|
||||
@Column({ type: 'varchar', length: 50, default: 'sensor' })
|
||||
source: string;
|
||||
|
||||
@ApiProperty({ description: 'Confidence level of the update (0-1)' })
|
||||
@Column({ type: 'decimal', precision: 3, scale: 2, default: 1.0 })
|
||||
confidence: number;
|
||||
|
||||
@ApiProperty({ description: 'Additional metadata for the update' })
|
||||
@Column({ type: 'jsonb', default: '{}' })
|
||||
metadata: Record<string, any>;
|
||||
|
||||
@ApiProperty({ description: 'Timestamp of the update' })
|
||||
@CreateDateColumn()
|
||||
timestamp: Date;
|
||||
|
||||
// Relations
|
||||
@ManyToOne(() => ParkingLot, (parkingLot) => parkingLot.updates)
|
||||
@JoinColumn({ name: 'parkingLotId' })
|
||||
parkingLot: ParkingLot;
|
||||
}
|
||||
179
backend/src/modules/parking/parking.controller.ts
Normal file
179
backend/src/modules/parking/parking.controller.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Param,
|
||||
Body,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
UseGuards,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
ApiBearerAuth,
|
||||
} from '@nestjs/swagger';
|
||||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { ParkingService } from './parking.service';
|
||||
import { FindNearbyParkingDto } from './dto/find-nearby-parking.dto';
|
||||
import { UpdateParkingAvailabilityDto } from './dto/update-availability.dto';
|
||||
import { ParkingLot } from './entities/parking-lot.entity';
|
||||
import { ParkingUpdate } from './entities/parking-update.entity';
|
||||
|
||||
@ApiTags('Parking')
|
||||
@Controller('parking')
|
||||
@UseGuards(ThrottlerGuard)
|
||||
export class ParkingController {
|
||||
constructor(private readonly parkingService: ParkingService) {}
|
||||
|
||||
@Post('nearby')
|
||||
@ApiOperation({
|
||||
summary: 'Find nearby parking lots',
|
||||
description: 'Search for parking lots within a specified radius of the given coordinates'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Successfully found nearby parking lots',
|
||||
type: ParkingLot,
|
||||
isArray: true,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Invalid coordinates or parameters',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
description: 'Failed to search for parking lots',
|
||||
})
|
||||
async findNearbyParking(@Body() dto: FindNearbyParkingDto) {
|
||||
return this.parkingService.findNearbyParking(dto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'Get all parking lots',
|
||||
description: 'Retrieve all active parking lots in the system'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Successfully retrieved parking lots',
|
||||
type: ParkingLot,
|
||||
isArray: true,
|
||||
})
|
||||
async getAllParkingLots(): Promise<ParkingLot[]> {
|
||||
return this.parkingService.getAllParkingLots();
|
||||
}
|
||||
|
||||
@Get('popular')
|
||||
@ApiOperation({
|
||||
summary: 'Get popular parking lots',
|
||||
description: 'Retrieve the most frequently visited parking lots'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'limit',
|
||||
required: false,
|
||||
description: 'Maximum number of results',
|
||||
example: 10
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Successfully retrieved popular parking lots',
|
||||
type: ParkingLot,
|
||||
isArray: true,
|
||||
})
|
||||
async getPopularParkingLots(@Query('limit') limit?: number): Promise<ParkingLot[]> {
|
||||
return this.parkingService.getPopularParkingLots(limit);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({
|
||||
summary: 'Get parking lot details',
|
||||
description: 'Retrieve detailed information about a specific parking lot'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'Parking lot ID',
|
||||
example: 1
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Successfully retrieved parking lot details',
|
||||
type: ParkingLot,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Parking lot not found',
|
||||
})
|
||||
async getParkingLotById(@Param('id', ParseIntPipe) id: number): Promise<ParkingLot> {
|
||||
return this.parkingService.findById(id);
|
||||
}
|
||||
|
||||
@Put(':id/availability')
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({
|
||||
summary: 'Update parking availability',
|
||||
description: 'Update the number of available slots for a parking lot'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'Parking lot ID',
|
||||
example: 1
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Successfully updated parking availability',
|
||||
type: ParkingLot,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Parking lot not found',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.BAD_REQUEST,
|
||||
description: 'Invalid availability data',
|
||||
})
|
||||
async updateAvailability(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() dto: UpdateParkingAvailabilityDto,
|
||||
): Promise<ParkingLot> {
|
||||
return this.parkingService.updateAvailability(id, dto);
|
||||
}
|
||||
|
||||
@Get(':id/history')
|
||||
@ApiOperation({
|
||||
summary: 'Get parking lot update history',
|
||||
description: 'Retrieve the update history for a specific parking lot'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: 'Parking lot ID',
|
||||
example: 1
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'limit',
|
||||
required: false,
|
||||
description: 'Maximum number of history records',
|
||||
example: 100
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: 'Successfully retrieved parking lot history',
|
||||
type: ParkingUpdate,
|
||||
isArray: true,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: 'Parking lot not found',
|
||||
})
|
||||
async getParkingLotHistory(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query('limit') limit?: number,
|
||||
): Promise<ParkingUpdate[]> {
|
||||
return this.parkingService.getParkingLotHistory(id, limit);
|
||||
}
|
||||
}
|
||||
21
backend/src/modules/parking/parking.module.ts
Normal file
21
backend/src/modules/parking/parking.module.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { ParkingController } from './parking.controller';
|
||||
import { ParkingService } from './parking.service';
|
||||
import { ParkingLot } from './entities/parking-lot.entity';
|
||||
import { ParkingHistory } from './entities/parking-history.entity';
|
||||
import { ParkingUpdate } from './entities/parking-update.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
ParkingLot,
|
||||
ParkingHistory,
|
||||
ParkingUpdate,
|
||||
]),
|
||||
],
|
||||
controllers: [ParkingController],
|
||||
providers: [ParkingService],
|
||||
exports: [ParkingService],
|
||||
})
|
||||
export class ParkingModule {}
|
||||
171
backend/src/modules/parking/parking.service.ts
Normal file
171
backend/src/modules/parking/parking.service.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { Injectable, Logger, NotFoundException, InternalServerErrorException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ParkingLot } from './entities/parking-lot.entity';
|
||||
import { ParkingUpdate } from './entities/parking-update.entity';
|
||||
import { FindNearbyParkingDto } from './dto/find-nearby-parking.dto';
|
||||
import { UpdateParkingAvailabilityDto } from './dto/update-availability.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ParkingService {
|
||||
private readonly logger = new Logger(ParkingService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ParkingLot)
|
||||
private readonly parkingRepository: Repository<ParkingLot>,
|
||||
@InjectRepository(ParkingUpdate)
|
||||
private readonly updateRepository: Repository<ParkingUpdate>,
|
||||
) {}
|
||||
|
||||
async findNearbyParking(dto: FindNearbyParkingDto): Promise<{
|
||||
parkingLots: ParkingLot[];
|
||||
userLocation: { lat: number; lng: number };
|
||||
searchRadius: number;
|
||||
}> {
|
||||
try {
|
||||
this.logger.debug(`Finding parking near ${dto.lat}, ${dto.lng} within ${dto.radius}m`);
|
||||
|
||||
let query = this.parkingRepository
|
||||
.createQueryBuilder('lot')
|
||||
.select([
|
||||
'lot.*',
|
||||
'ST_Distance(lot.location::geography, ST_Point(:lng, :lat)::geography) as distance'
|
||||
])
|
||||
.where(
|
||||
'ST_DWithin(lot.location::geography, ST_Point(:lng, :lat)::geography, :radius)',
|
||||
{
|
||||
lng: dto.lng,
|
||||
lat: dto.lat,
|
||||
radius: dto.radius,
|
||||
}
|
||||
)
|
||||
.andWhere('lot.isActive = :isActive', { isActive: true });
|
||||
|
||||
// Apply price filter
|
||||
if (dto.priceRange && dto.priceRange.length === 2) {
|
||||
query = query.andWhere(
|
||||
'lot.hourlyRate BETWEEN :minPrice AND :maxPrice',
|
||||
{ minPrice: dto.priceRange[0], maxPrice: dto.priceRange[1] }
|
||||
);
|
||||
}
|
||||
|
||||
// Apply amenities filter
|
||||
if (dto.amenities && dto.amenities.length > 0) {
|
||||
dto.amenities.forEach((amenity, index) => {
|
||||
query = query.andWhere(
|
||||
`lot.amenities ? :amenity${index}`,
|
||||
{ [`amenity${index}`]: amenity }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Apply availability filter
|
||||
if (dto.availabilityFilter) {
|
||||
switch (dto.availabilityFilter) {
|
||||
case 'available':
|
||||
query = query.andWhere('(lot.availableSlots::float / lot.totalSlots::float) > 0.2');
|
||||
break;
|
||||
case 'limited':
|
||||
query = query.andWhere('(lot.availableSlots::float / lot.totalSlots::float) BETWEEN 0.05 AND 0.2');
|
||||
break;
|
||||
case 'full':
|
||||
query = query.andWhere('(lot.availableSlots::float / lot.totalSlots::float) < 0.05');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const results = await query
|
||||
.orderBy('distance', 'ASC')
|
||||
.limit(dto.maxResults)
|
||||
.getRawMany();
|
||||
|
||||
// Transform raw results back to entities with distance
|
||||
const parkingLots = (results as any[]).map((result: any) => {
|
||||
const { distance, ...lotData } = result;
|
||||
const lot = this.parkingRepository.create(lotData);
|
||||
(lot as any).distance = parseFloat(distance);
|
||||
return lot;
|
||||
}) as unknown as ParkingLot[];
|
||||
|
||||
return {
|
||||
parkingLots,
|
||||
userLocation: { lat: dto.lat, lng: dto.lng },
|
||||
searchRadius: dto.radius,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to find nearby parking', error);
|
||||
throw new InternalServerErrorException('Failed to find nearby parking');
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: number): Promise<ParkingLot> {
|
||||
const lot = await this.parkingRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['updates'],
|
||||
});
|
||||
|
||||
if (!lot) {
|
||||
throw new NotFoundException(`Parking lot with ID ${id} not found`);
|
||||
}
|
||||
|
||||
return lot;
|
||||
}
|
||||
|
||||
async updateAvailability(
|
||||
id: number,
|
||||
dto: UpdateParkingAvailabilityDto
|
||||
): Promise<ParkingLot> {
|
||||
const lot = await this.findById(id);
|
||||
|
||||
// Create update record
|
||||
const update = this.updateRepository.create({
|
||||
parkingLotId: id,
|
||||
availableSlots: dto.availableSlots,
|
||||
source: dto.source,
|
||||
confidence: dto.confidence,
|
||||
metadata: dto.metadata,
|
||||
});
|
||||
|
||||
await this.updateRepository.save(update);
|
||||
|
||||
// Update parking lot
|
||||
lot.availableSlots = dto.availableSlots;
|
||||
lot.updatedAt = new Date();
|
||||
|
||||
return this.parkingRepository.save(lot);
|
||||
}
|
||||
|
||||
async getAllParkingLots(): Promise<ParkingLot[]> {
|
||||
return this.parkingRepository.find({
|
||||
where: { isActive: true },
|
||||
order: { name: 'ASC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getParkingLotHistory(id: number, limit: number = 100): Promise<ParkingUpdate[]> {
|
||||
return this.updateRepository.find({
|
||||
where: { parkingLotId: id },
|
||||
order: { timestamp: 'DESC' },
|
||||
take: limit,
|
||||
});
|
||||
}
|
||||
|
||||
async getPopularParkingLots(limit: number = 10): Promise<ParkingLot[]> {
|
||||
const results = await this.parkingRepository
|
||||
.createQueryBuilder('lot')
|
||||
.leftJoin('lot.history', 'history')
|
||||
.select(['lot.*', 'COUNT(history.id) as visit_count'])
|
||||
.where('lot.isActive = :isActive', { isActive: true })
|
||||
.groupBy('lot.id')
|
||||
.orderBy('visit_count', 'DESC')
|
||||
.limit(limit)
|
||||
.getRawMany();
|
||||
|
||||
return (results as any[]).map((result: any) => {
|
||||
const { visit_count, ...lotData } = result;
|
||||
const lot = this.parkingRepository.create(lotData);
|
||||
(lot as any).visitCount = parseInt(visit_count) || 0;
|
||||
return lot;
|
||||
}) as unknown as ParkingLot[];
|
||||
}
|
||||
}
|
||||
124
backend/src/modules/routing/dto/route-request.dto.ts
Normal file
124
backend/src/modules/routing/dto/route-request.dto.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
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;
|
||||
}
|
||||
69
backend/src/modules/routing/routing.controller.ts
Normal file
69
backend/src/modules/routing/routing.controller.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
10
backend/src/modules/routing/routing.module.ts
Normal file
10
backend/src/modules/routing/routing.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
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 {}
|
||||
232
backend/src/modules/routing/routing.service.ts
Normal file
232
backend/src/modules/routing/routing.service.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
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',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
50
backend/src/modules/users/entities/user.entity.ts
Normal file
50
backend/src/modules/users/entities/user.entity.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ParkingHistory } from '../../parking/entities/parking-history.entity';
|
||||
|
||||
@Entity('users')
|
||||
export class User {
|
||||
@ApiProperty({ description: 'Unique identifier for the user' })
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: 'User email address' })
|
||||
@Column({ type: 'varchar', length: 255, unique: true, nullable: true })
|
||||
email: string;
|
||||
|
||||
@ApiProperty({ description: 'User full name' })
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: 'Hashed password' })
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
password: string;
|
||||
|
||||
@ApiProperty({ description: 'User preferences and settings' })
|
||||
@Column({ type: 'jsonb', default: '{}' })
|
||||
preferences: Record<string, any>;
|
||||
|
||||
@ApiProperty({ description: 'Whether the user account is active' })
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@ApiProperty({ description: 'User creation timestamp' })
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
// Relations
|
||||
@OneToMany(() => ParkingHistory, (history) => history.user)
|
||||
parkingHistory: ParkingHistory[];
|
||||
|
||||
// Methods
|
||||
toJSON() {
|
||||
const { password, ...result } = this;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
17
backend/src/modules/users/users.controller.ts
Normal file
17
backend/src/modules/users/users.controller.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { UsersService } from './users.service';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@ApiTags('Users')
|
||||
@Controller('users')
|
||||
export class UsersController {
|
||||
constructor(private readonly usersService: UsersService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: 'Get all users' })
|
||||
@ApiResponse({ status: 200, description: 'Successfully retrieved users', type: User, isArray: true })
|
||||
async findAll(): Promise<User[]> {
|
||||
return this.usersService.findAll();
|
||||
}
|
||||
}
|
||||
13
backend/src/modules/users/users.module.ts
Normal file
13
backend/src/modules/users/users.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { UsersController } from './users.controller';
|
||||
import { UsersService } from './users.service';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([User])],
|
||||
controllers: [UsersController],
|
||||
providers: [UsersService],
|
||||
exports: [UsersService],
|
||||
})
|
||||
export class UsersModule {}
|
||||
38
backend/src/modules/users/users.service.ts
Normal file
38
backend/src/modules/users/users.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { User } from './entities/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
@InjectRepository(User)
|
||||
private readonly userRepository: Repository<User>,
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<User[]> {
|
||||
return this.userRepository.find();
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ where: { id } });
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
return this.userRepository.findOne({ where: { email } });
|
||||
}
|
||||
|
||||
async create(userData: Partial<User>): Promise<User> {
|
||||
const user = this.userRepository.create(userData);
|
||||
return this.userRepository.save(user);
|
||||
}
|
||||
|
||||
async update(id: string, userData: Partial<User>): Promise<User> {
|
||||
await this.userRepository.update(id, userData);
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.userRepository.delete(id);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user