feat(tactical): add isochrone emergency reachability endpoint and clean build config
This commit is contained in:
@@ -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 / حفظ سيناريو الرموز التكتيكية',
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user