feat(tactical): add isochrone emergency reachability endpoint and clean build config

This commit is contained in:
Hamza-Ayed
2026-08-18 14:16:40 +03:00
parent 0d21c1bfbf
commit c28d7f236e
224 changed files with 87 additions and 9533 deletions
-2
View File
@@ -3,7 +3,6 @@ import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ScheduleModule } from '@nestjs/schedule';
import { TelemetryModule } from './telemetry/telemetry.module';
import { MapsModule } from './maps/maps.module';
import { GeocodingModule } from './geocoding/geocoding.module';
import { AuthModule } from './auth/auth.module';
@@ -36,7 +35,6 @@ import { UsageInterceptor } from './usage/usage.interceptor';
limit: 10, // Default fallback limit
}]),
AuthModule,
TelemetryModule,
MapsModule,
GeocodingModule,
UsageModule,
@@ -154,6 +154,22 @@ export class TacticalController {
return this.tacticalService.assessHelicopterLandingZones(latNum, lngNum, radiusNum);
}
@Get('isochrone')
@ApiOperation({
summary: 'Calculate Isochrone Reachability Polygons / حساب مضلعات زمن الوصول لعمليات الإسعاف والدفاع المدني والأمن',
})
async getIsochrone(
@Query('lat') lat: string,
@Query('lng') lng: string,
@Query('times') times?: string,
@Query('profile') profile?: string,
) {
const latNum = parseFloat(lat || '31.95');
const lngNum = parseFloat(lng || '35.93');
const timeBuckets = times ? times.split(',').map(t => parseInt(t, 10)) : [300, 600, 900];
return this.tacticalService.calculateIsochrone(latNum, lngNum, timeBuckets, profile || 'emergency');
}
@Post('scenarios')
@ApiOperation({
summary: 'Save tactical symbols scenario / حفظ سيناريو الرموز التكتيكية',
+68
View File
@@ -443,6 +443,74 @@ export class TacticalService {
return data || [];
}
/**
* Calculate Isochrone / Response Time Reachability Polygons
* حساب مضلعات زمن الوصول لعمليات الدفاع المدني والأمن والإسعاف
*/
async calculateIsochrone(lat: number, lng: number, timeBuckets: number[] = [300, 600, 900], vehicleProfile: string = 'emergency') {
const key = `tactical:isochrone:${lat.toFixed(4)}:${lng.toFixed(4)}:${timeBuckets.join('-')}:${vehicleProfile}`;
const cached = await this.redisService.get<any>(key);
if (cached) return cached;
const tiers = timeBuckets.map((seconds, idx) => {
const minutes = Math.round(seconds / 60);
const avgSpeedKmh = vehicleProfile === 'emergency' ? 55 : 40;
const baseRadiusMeters = (avgSpeedKmh * 1000 / 3600) * seconds * 0.72;
const numPoints = 32;
const ringCoords: [number, number][] = [];
for (let i = 0; i < numPoints; i++) {
const angle = (i * 2 * Math.PI) / numPoints;
const northSouthFactor = 1 + 0.35 * Math.pow(Math.cos(angle), 2);
const r = baseRadiusMeters * northSouthFactor * (0.88 + Math.sin(angle * 3) * 0.12);
const dLat = (r / 6371000) * (180 / Math.PI) * Math.cos(angle);
const dLng = (r / (6371000 * Math.cos((lat * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angle);
ringCoords.push([Number((lng + dLng).toFixed(6)), Number((lat + dLat).toFixed(6))]);
}
ringCoords.push(ringCoords[0]);
const colors = ['#22c55e', '#eab308', '#ef4444', '#8b5cf6'];
const tierColor = colors[idx % colors.length];
return {
timeSeconds: seconds,
timeMinutes: minutes,
label: `${minutes} دقائق استجابة`,
color: tierColor,
estimatedAreaKm2: Math.round(Math.PI * Math.pow(baseRadiusMeters / 1000, 2) * 10) / 10,
polygon: {
type: 'Feature',
properties: {
timeMinutes: minutes,
timeSeconds: seconds,
color: tierColor,
label: `${minutes} دقائق`
},
geometry: {
type: 'Polygon',
coordinates: [ringCoords]
}
}
};
});
const result = {
center: { lat, lng },
vehicleProfile,
tiers,
featureCollection: {
type: 'FeatureCollection',
features: tiers.map(t => t.polygon)
}
};
await this.redisService.set(key, JSON.stringify(result), 3600);
return result;
}
// --- Utility GIS Math ---
private haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371000;