chore: add build artifacts, map assets, and routing data
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsNumber, IsOptional, IsString, Max, Min, IsArray, ValidateNested } from 'class-validator';
|
||||
import { Type, Transform } from 'class-transformer';
|
||||
|
||||
export class DriverTelemetryDto {
|
||||
@ApiProperty({ description: 'Driver unique ID', example: 'driver_jo_1042' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Transform(({ obj, value }) => value ?? obj.driver_id ?? obj.driverId)
|
||||
driver_id: string;
|
||||
|
||||
@ApiProperty({ description: 'Latitude coordinate (-90 to 90)', example: 31.9539 })
|
||||
@IsNumber()
|
||||
@Min(-90)
|
||||
@Max(90)
|
||||
@Transform(({ obj, value }) => Number(value ?? obj.latitude ?? obj.lat))
|
||||
latitude: number;
|
||||
|
||||
@ApiProperty({ description: 'Longitude coordinate (-180 to 180)', example: 35.9106 })
|
||||
@IsNumber()
|
||||
@Min(-180)
|
||||
@Max(180)
|
||||
@Transform(({ obj, value }) => Number(value ?? obj.longitude ?? obj.lng))
|
||||
longitude: number;
|
||||
|
||||
@ApiProperty({ description: 'Instantaneous vehicle speed in km/h', example: 45.5 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Transform(({ obj, value }) => Number(value ?? obj.speed ?? 0))
|
||||
speed: number;
|
||||
|
||||
@ApiProperty({ description: 'Compass heading / bearing in degrees (0 - 360)', example: 185.0 })
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
@Max(360)
|
||||
@Transform(({ obj, value }) => Number(value ?? obj.heading ?? 0))
|
||||
heading: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Distance traveled in meters', example: 1250.4, default: 0 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Transform(({ obj, value }) => (value != null ? Number(value) : (obj.distance != null ? Number(obj.distance) : 0)))
|
||||
distance?: number;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Elevation above mean sea level in meters (AMSL)', example: 890.5, default: 0 })
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Transform(({ obj, value }) => {
|
||||
const val = value ?? obj.elevation ?? obj.altitude;
|
||||
return val != null ? Number(val) : 0;
|
||||
})
|
||||
elevation?: number;
|
||||
}
|
||||
|
||||
export class DriverTelemetryBatchDto {
|
||||
@ApiProperty({ type: [DriverTelemetryDto], description: 'Array of telemetry points for batch processing' })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DriverTelemetryDto)
|
||||
points: DriverTelemetryDto[];
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Controller, Post, Get, Body, Query, Param, UseGuards, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiQuery, ApiParam, ApiSecurity } from '@nestjs/swagger';
|
||||
import { TelemetryService } from './telemetry.service';
|
||||
import { DriverTelemetryDto, DriverTelemetryBatchDto } from './dto/driver-telemetry.dto';
|
||||
import { ApiKeyGuard } from '../common/guards/api-key.guard';
|
||||
|
||||
@ApiTags('telemetry')
|
||||
@ApiSecurity('x-api-key')
|
||||
@Controller('telemetry')
|
||||
@UseGuards(ApiKeyGuard)
|
||||
export class TelemetryController {
|
||||
constructor(private readonly telemetryService: TelemetryService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
summary: 'Ingest real-time driver telemetry with elevation & distance 📡⛰️',
|
||||
description: 'Receives GPS position, speed, heading, distance, and AMSL elevation from driver app.',
|
||||
})
|
||||
async ingest(@Body() data: DriverTelemetryDto) {
|
||||
if (!data.driver_id) {
|
||||
throw new HttpException('Missing driver_id', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return this.telemetryService.ingest(data);
|
||||
}
|
||||
|
||||
@Post('batch')
|
||||
@ApiOperation({
|
||||
summary: 'Batch ingest driver telemetry points 📦',
|
||||
description: 'Receives an array of telemetry points for offline-buffered sync or high-frequency traces.',
|
||||
})
|
||||
async ingestBatch(@Body() body: DriverTelemetryBatchDto) {
|
||||
if (!body || !Array.isArray(body.points)) {
|
||||
throw new HttpException('Invalid payload: expected { points: [...] }', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return this.telemetryService.ingestBatch(body.points);
|
||||
}
|
||||
|
||||
@Get('nearby')
|
||||
@ApiOperation({ summary: 'Query active drivers within spatial radius with elevation & bearing 🚗' })
|
||||
@ApiQuery({ name: 'lat', required: true, type: Number, description: 'Center latitude' })
|
||||
@ApiQuery({ name: 'lng', required: true, type: Number, description: 'Center longitude' })
|
||||
@ApiQuery({ name: 'radius', required: false, type: Number, description: 'Radius in meters (default: 5000m)' })
|
||||
async getNearby(
|
||||
@Query('lat') lat: number,
|
||||
@Query('lng') lng: number,
|
||||
@Query('radius') radius?: number,
|
||||
) {
|
||||
const latNum = Number(lat);
|
||||
const lngNum = Number(lng);
|
||||
if (isNaN(latNum) || isNaN(lngNum)) {
|
||||
throw new HttpException('lat and lng must be valid numbers', HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
return this.telemetryService.getRecentDrivers(latNum, lngNum, radius ? Number(radius) : 5000);
|
||||
}
|
||||
|
||||
@Get('driver/:driverId/profile')
|
||||
@ApiOperation({ summary: 'Get 3D elevation profile, climb, and terrain grade for a driver 📈' })
|
||||
@ApiParam({ name: 'driverId', required: true, description: 'Driver unique ID' })
|
||||
@ApiQuery({ name: 'hours', required: false, description: 'Window in hours (default: 24)' })
|
||||
async getDriverElevationProfile(
|
||||
@Param('driverId') driverId: string,
|
||||
@Query('hours') hours?: number,
|
||||
) {
|
||||
return this.telemetryService.getElevationProfile(driverId, hours ? Number(hours) : 24);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Entity, Column, PrimaryGeneratedColumn, Index, CreateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('telemetry_logs')
|
||||
@Index(['driverId', 'timestamp'], { unique: false })
|
||||
export class TelemetryLog {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ name: 'driverId' })
|
||||
@Index()
|
||||
driverId: string;
|
||||
|
||||
@Column('decimal', { precision: 10, scale: 7 })
|
||||
latitude: number;
|
||||
|
||||
@Column('decimal', { precision: 10, scale: 7 })
|
||||
longitude: number;
|
||||
|
||||
@Column('float', { default: 0 })
|
||||
speed: number;
|
||||
|
||||
@Column('float', { default: 0 })
|
||||
heading: number;
|
||||
|
||||
// Cumulative or step distance traveled in meters (المسافة المقطوعة بالمتر)
|
||||
@Column('float', { default: 0 })
|
||||
distance: number;
|
||||
|
||||
// Elevation above mean sea level in meters (الارتفاع عن مستوى سطح البحر بالمتر AMSL)
|
||||
@Column('float', { default: 0 })
|
||||
elevation: number;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamp with time zone' })
|
||||
@Index()
|
||||
timestamp: Date;
|
||||
|
||||
// PostGIS spatial point for ultra-fast spatial and proximity indexing
|
||||
@Column({
|
||||
type: 'geography',
|
||||
spatialFeatureType: 'Point',
|
||||
srid: 4326,
|
||||
nullable: true,
|
||||
})
|
||||
@Index({ spatial: true })
|
||||
location: any;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { TelemetryLog } from './telemetry.entity';
|
||||
import { TelemetryService } from './telemetry.service';
|
||||
import { TelemetryController } from './telemetry.controller';
|
||||
import { RedisModule } from '../common/redis.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([TelemetryLog]),
|
||||
RedisModule,
|
||||
],
|
||||
controllers: [TelemetryController],
|
||||
providers: [TelemetryService],
|
||||
exports: [TelemetryService],
|
||||
})
|
||||
export class TelemetryModule {}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { TelemetryService } from './telemetry.service';
|
||||
import { TelemetryLog } from './telemetry.entity';
|
||||
import { RedisService } from '../common/redis.service';
|
||||
|
||||
describe('TelemetryService', () => {
|
||||
let service: TelemetryService;
|
||||
let mockRepo: any;
|
||||
let mockDataSource: any;
|
||||
let mockRedis: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockRepo = {
|
||||
create: jest.fn().mockImplementation((dto) => ({ id: 42, ...dto })),
|
||||
save: jest.fn().mockImplementation((entity) => Promise.resolve({ id: 42, ...entity })),
|
||||
};
|
||||
|
||||
mockDataSource = {
|
||||
query: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
|
||||
mockRedis = {
|
||||
set: jest.fn().mockResolvedValue(undefined),
|
||||
get: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
TelemetryService,
|
||||
{
|
||||
provide: getRepositoryToken(TelemetryLog),
|
||||
useValue: mockRepo,
|
||||
},
|
||||
{
|
||||
provide: DataSource,
|
||||
useValue: mockDataSource,
|
||||
},
|
||||
{
|
||||
provide: RedisService,
|
||||
useValue: mockRedis,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<TelemetryService>(TelemetryService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
it('should ingest telemetry with elevation, distance, speed, and heading', async () => {
|
||||
const payload = {
|
||||
driver_id: 'test_driver_77',
|
||||
latitude: 31.9539,
|
||||
longitude: 35.9106,
|
||||
speed: 60.5,
|
||||
heading: 180.0,
|
||||
distance: 1450.0,
|
||||
elevation: 920.4,
|
||||
};
|
||||
|
||||
const result = await service.ingest(payload);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.driver_id).toBe('test_driver_77');
|
||||
expect(result.elevation).toBe(920.4);
|
||||
expect(result.distance).toBe(1450.0);
|
||||
|
||||
expect(mockRepo.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
driverId: 'test_driver_77',
|
||||
latitude: 31.9539,
|
||||
longitude: 35.9106,
|
||||
speed: 60.5,
|
||||
heading: 180.0,
|
||||
distance: 1450.0,
|
||||
elevation: 920.4,
|
||||
}),
|
||||
);
|
||||
expect(mockRepo.save).toHaveBeenCalled();
|
||||
expect(mockRedis.set).toHaveBeenCalledWith(
|
||||
'fleet:driver:test_driver_77:live',
|
||||
expect.objectContaining({
|
||||
driverId: 'test_driver_77',
|
||||
elevation: 920.4,
|
||||
distance: 1450.0,
|
||||
}),
|
||||
900,
|
||||
);
|
||||
});
|
||||
|
||||
it('should batch ingest multiple telemetry points with elevation', async () => {
|
||||
const batch = [
|
||||
{
|
||||
driver_id: 'd1',
|
||||
latitude: 31.95,
|
||||
longitude: 35.91,
|
||||
speed: 50,
|
||||
heading: 90,
|
||||
distance: 100,
|
||||
elevation: 900,
|
||||
},
|
||||
{
|
||||
driver_id: 'd2',
|
||||
latitude: 31.96,
|
||||
longitude: 35.92,
|
||||
speed: 55,
|
||||
heading: 95,
|
||||
distance: 120,
|
||||
elevation: 915,
|
||||
},
|
||||
];
|
||||
|
||||
const res = await service.ingestBatch(batch);
|
||||
expect(res.success).toBe(true);
|
||||
expect(res.count).toBe(2);
|
||||
expect(mockRepo.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,278 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, DataSource } from 'typeorm';
|
||||
import { TelemetryLog } from './telemetry.entity';
|
||||
import { DriverTelemetryDto } from './dto/driver-telemetry.dto';
|
||||
import { RedisService } from '../common/redis.service';
|
||||
|
||||
@Injectable()
|
||||
export class TelemetryService implements OnModuleInit {
|
||||
private readonly logger = new Logger(TelemetryService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(TelemetryLog)
|
||||
private readonly telemetryRepo: Repository<TelemetryLog>,
|
||||
private readonly dataSource: DataSource,
|
||||
private readonly redisService: RedisService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
try {
|
||||
// Ensure PostGIS extension and telemetry_logs table columns exist
|
||||
await this.dataSource.query(`
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE TABLE IF NOT EXISTS telemetry_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
"driverId" VARCHAR(255) NOT NULL,
|
||||
latitude NUMERIC(10, 7) NOT NULL,
|
||||
longitude NUMERIC(10, 7) NOT NULL,
|
||||
speed FLOAT NOT NULL DEFAULT 0,
|
||||
heading FLOAT NOT NULL DEFAULT 0,
|
||||
distance FLOAT DEFAULT 0,
|
||||
elevation FLOAT DEFAULT 0,
|
||||
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
location GEOGRAPHY(Point, 4326)
|
||||
);
|
||||
ALTER TABLE telemetry_logs ADD COLUMN IF NOT EXISTS distance FLOAT DEFAULT 0;
|
||||
ALTER TABLE telemetry_logs ADD COLUMN IF NOT EXISTS elevation FLOAT DEFAULT 0;
|
||||
CREATE INDEX IF NOT EXISTS telemetry_logs_driver_idx ON telemetry_logs ("driverId");
|
||||
CREATE INDEX IF NOT EXISTS telemetry_logs_timestamp_idx ON telemetry_logs (timestamp DESC);
|
||||
CREATE INDEX IF NOT EXISTS telemetry_logs_location_idx ON telemetry_logs USING GIST (location);
|
||||
`);
|
||||
this.logger.log('✅ Telemetry schema verified: elevation and distance columns ready.');
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Telemetry DB auto-migration check note: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ingest a single driver telemetry record including elevation & distance
|
||||
*/
|
||||
async ingest(data: DriverTelemetryDto) {
|
||||
const lat = Number(data.latitude);
|
||||
const lng = Number(data.longitude);
|
||||
const speed = Number(data.speed || 0);
|
||||
const heading = Number(data.heading || 0);
|
||||
const distance = Number(data.distance || 0);
|
||||
const elevation = Number(data.elevation || 0);
|
||||
|
||||
const log = this.telemetryRepo.create({
|
||||
driverId: data.driver_id,
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
speed,
|
||||
heading,
|
||||
distance,
|
||||
elevation,
|
||||
timestamp: new Date(),
|
||||
location: {
|
||||
type: 'Point',
|
||||
coordinates: [lng, lat],
|
||||
},
|
||||
});
|
||||
|
||||
const saved = await this.telemetryRepo.save(log);
|
||||
|
||||
// Fast memory caching in Redis for real-time fleet queries (TTL 15 minutes)
|
||||
try {
|
||||
await this.redisService.set(
|
||||
`fleet:driver:${data.driver_id}:live`,
|
||||
{
|
||||
driverId: data.driver_id,
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
speed,
|
||||
heading,
|
||||
distance,
|
||||
elevation,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
900,
|
||||
);
|
||||
} catch (_) {
|
||||
// Redis failover - non-blocking
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
id: saved.id,
|
||||
driver_id: data.driver_id,
|
||||
elevation,
|
||||
distance,
|
||||
timestamp: saved.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch ingest multiple telemetry points
|
||||
*/
|
||||
async ingestBatch(points: DriverTelemetryDto[]) {
|
||||
if (!points || points.length === 0) {
|
||||
return { success: true, count: 0 };
|
||||
}
|
||||
|
||||
const entities = points.map((p) => {
|
||||
const lat = Number(p.latitude);
|
||||
const lng = Number(p.longitude);
|
||||
const speed = Number(p.speed || 0);
|
||||
const heading = Number(p.heading || 0);
|
||||
const distance = Number(p.distance || 0);
|
||||
const elevation = Number(p.elevation || 0);
|
||||
|
||||
return this.telemetryRepo.create({
|
||||
driverId: p.driver_id,
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
speed,
|
||||
heading,
|
||||
distance,
|
||||
elevation,
|
||||
timestamp: new Date(),
|
||||
location: {
|
||||
type: 'Point',
|
||||
coordinates: [lng, lat],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await this.telemetryRepo.save(entities);
|
||||
|
||||
// Update Redis cache for the latest point of each driver
|
||||
try {
|
||||
const latestByDriver = new Map<string, DriverTelemetryDto>();
|
||||
for (const p of points) {
|
||||
latestByDriver.set(p.driver_id, p);
|
||||
}
|
||||
for (const [dId, p] of latestByDriver.entries()) {
|
||||
await this.redisService.set(
|
||||
`fleet:driver:${dId}:live`,
|
||||
{
|
||||
driverId: dId,
|
||||
latitude: Number(p.latitude),
|
||||
longitude: Number(p.longitude),
|
||||
speed: Number(p.speed || 0),
|
||||
heading: Number(p.heading || 0),
|
||||
distance: Number(p.distance || 0),
|
||||
elevation: Number(p.elevation || 0),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
900,
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
count: entities.length,
|
||||
timestamp: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find nearby active drivers using PostGIS spatial geography search
|
||||
*/
|
||||
async getRecentDrivers(lat: number, lng: number, radiusMeters: number = 5000) {
|
||||
const rows = await this.dataSource.query(
|
||||
`SELECT DISTINCT ON ("driverId")
|
||||
id, "driverId", latitude, longitude, speed, heading, distance, elevation, timestamp,
|
||||
ST_Distance(location, ST_MakePoint($1, $2)::geography) as distance_to_center_meters
|
||||
FROM telemetry_logs
|
||||
WHERE ST_DWithin(location, ST_MakePoint($1, $2)::geography, $3)
|
||||
AND timestamp >= NOW() - INTERVAL '4 hours'
|
||||
ORDER BY "driverId", timestamp DESC
|
||||
LIMIT 100`,
|
||||
[lng, lat, radiusMeters],
|
||||
);
|
||||
|
||||
return rows.map((r: any) => ({
|
||||
driver_id: r.driverId,
|
||||
latitude: parseFloat(r.latitude),
|
||||
longitude: parseFloat(r.longitude),
|
||||
speed: parseFloat(r.speed),
|
||||
heading: parseFloat(r.heading),
|
||||
distance: parseFloat(r.distance || 0),
|
||||
elevation: parseFloat(r.elevation || 0),
|
||||
timestamp: r.timestamp,
|
||||
distance_to_center_meters: parseFloat(r.distance_to_center_meters),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate 3D elevation profile and vertical gradient for a specific driver
|
||||
*/
|
||||
async getElevationProfile(driverId: string, hours: number = 24) {
|
||||
const points = await this.dataSource.query(
|
||||
`SELECT latitude, longitude, speed, heading, distance, elevation, timestamp
|
||||
FROM telemetry_logs
|
||||
WHERE "driverId" = $1
|
||||
AND timestamp >= NOW() - ($2 || ' hours')::interval
|
||||
ORDER BY timestamp ASC`,
|
||||
[driverId, hours],
|
||||
);
|
||||
|
||||
if (points.length === 0) {
|
||||
return {
|
||||
driver_id: driverId,
|
||||
hours,
|
||||
pointsCount: 0,
|
||||
minElevation: 0,
|
||||
maxElevation: 0,
|
||||
avgElevation: 0,
|
||||
totalClimbMeters: 0,
|
||||
totalDescentMeters: 0,
|
||||
maxGradePercent: 0,
|
||||
points: [],
|
||||
};
|
||||
}
|
||||
|
||||
let minElev = points[0].elevation || 0;
|
||||
let maxElev = points[0].elevation || 0;
|
||||
let sumElev = 0;
|
||||
let totalClimb = 0;
|
||||
let totalDescent = 0;
|
||||
let maxGrade = 0;
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const elev = parseFloat(points[i].elevation || '0');
|
||||
sumElev += elev;
|
||||
if (elev < minElev) minElev = elev;
|
||||
if (elev > maxElev) maxElev = elev;
|
||||
|
||||
if (i > 0) {
|
||||
const prevElev = parseFloat(points[i - 1].elevation || '0');
|
||||
const diff = elev - prevElev;
|
||||
if (diff > 0) totalClimb += diff;
|
||||
if (diff < 0) totalDescent += Math.abs(diff);
|
||||
|
||||
// Approximate grade percent if distance step is available
|
||||
const stepDist = parseFloat(points[i].distance || '0') - parseFloat(points[i - 1].distance || '0');
|
||||
if (stepDist > 10) {
|
||||
const grade = (Math.abs(diff) / stepDist) * 100;
|
||||
if (grade > maxGrade && grade < 50) {
|
||||
maxGrade = grade;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
driver_id: driverId,
|
||||
hours,
|
||||
pointsCount: points.length,
|
||||
minElevation: Math.round(minElev * 10) / 10,
|
||||
maxElevation: Math.round(maxElev * 10) / 10,
|
||||
avgElevation: Math.round((sumElev / points.length) * 10) / 10,
|
||||
totalClimbMeters: Math.round(totalClimb * 10) / 10,
|
||||
totalDescentMeters: Math.round(totalDescent * 10) / 10,
|
||||
maxGradePercent: Math.round(maxGrade * 10) / 10,
|
||||
recentPoints: points.slice(-30).map((p: any) => ({
|
||||
latitude: parseFloat(p.latitude),
|
||||
longitude: parseFloat(p.longitude),
|
||||
speed: parseFloat(p.speed),
|
||||
heading: parseFloat(p.heading),
|
||||
elevation: parseFloat(p.elevation),
|
||||
timestamp: p.timestamp,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user