fix(tactical): resolve DTO types and TS1272 build error for isolatedModules

This commit is contained in:
Hamza-Ayed
2026-08-18 09:46:04 +03:00
parent bd287d155b
commit 941420f5c0
86 changed files with 3614 additions and 1556 deletions
+4
View File
@@ -20,6 +20,8 @@ const throttler_1 = require("@nestjs/throttler");
const usage_module_1 = require("./usage/usage.module");
const billing_module_1 = require("./billing/billing.module");
const mail_module_1 = require("./common/mail.module");
const weather_module_1 = require("./weather/weather.module");
const tactical_module_1 = require("./tactical/tactical.module");
const usage_interceptor_1 = require("./usage/usage.interceptor");
let AppModule = class AppModule {
};
@@ -52,6 +54,8 @@ exports.AppModule = AppModule = __decorate([
usage_module_1.UsageModule,
billing_module_1.BillingModule,
mail_module_1.MailModule,
weather_module_1.WeatherModule,
tactical_module_1.TacticalModule,
],
controllers: [],
providers: [
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,uCAA0D;AAC1D,2CAA6D;AAC7D,6CAAgD;AAChD,+CAAkD;AAClD,mEAA+D;AAC/D,oDAAgD;AAChD,mEAA+D;AAC/D,oDAAgD;AAChD,iDAAoD;AACpD,uDAAmD;AACnD,6DAAyD;AACzD,sDAAkD;AAClD,iEAA6D;AAsCtD,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IApCrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC;gBACnB,QAAQ,EAAE,IAAI;aACf,CAAC;YACF,yBAAc,CAAC,OAAO,EAAE;YACxB,uBAAa,CAAC,YAAY,CAAC;gBACzB,OAAO,EAAE,CAAC,qBAAY,CAAC;gBACvB,MAAM,EAAE,CAAC,sBAAa,CAAC;gBACvB,UAAU,EAAE,CAAC,MAAqB,EAAE,EAAE,CAAC,CAAC;oBACtC,IAAI,EAAE,UAAU;oBAChB,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC;oBAC/B,gBAAgB,EAAE,IAAI;oBACtB,WAAW,EAAE,KAAK;iBACnB,CAAC;aACH,CAAC;YACF,2BAAe,CAAC,OAAO,CAAC,CAAC;oBACvB,GAAG,EAAE,KAAK;oBACV,KAAK,EAAE,EAAE;iBACV,CAAC,CAAC;YACH,wBAAU;YACV,kCAAe;YACf,wBAAU;YACV,kCAAe;YACf,0BAAW;YACX,8BAAa;YACb,wBAAU;SACX;QACD,WAAW,EAAE,EAAE;QACf,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,sBAAe;gBACxB,QAAQ,EAAE,oCAAgB;aAC3B;SACF;KACF,CAAC;GACW,SAAS,CAAG"}
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,uCAA0D;AAC1D,2CAA6D;AAC7D,6CAAgD;AAChD,+CAAkD;AAClD,mEAA+D;AAC/D,oDAAgD;AAChD,mEAA+D;AAC/D,oDAAgD;AAChD,iDAAoD;AACpD,uDAAmD;AACnD,6DAAyD;AACzD,sDAAkD;AAClD,6DAAyD;AACzD,gEAA4D;AAC5D,iEAA6D;AAwCtD,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IAtCrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC;gBACnB,QAAQ,EAAE,IAAI;aACf,CAAC;YACF,yBAAc,CAAC,OAAO,EAAE;YACxB,uBAAa,CAAC,YAAY,CAAC;gBACzB,OAAO,EAAE,CAAC,qBAAY,CAAC;gBACvB,MAAM,EAAE,CAAC,sBAAa,CAAC;gBACvB,UAAU,EAAE,CAAC,MAAqB,EAAE,EAAE,CAAC,CAAC;oBACtC,IAAI,EAAE,UAAU;oBAChB,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC;oBAC/B,gBAAgB,EAAE,IAAI;oBACtB,WAAW,EAAE,KAAK;iBACnB,CAAC;aACH,CAAC;YACF,2BAAe,CAAC,OAAO,CAAC,CAAC;oBACvB,GAAG,EAAE,KAAK;oBACV,KAAK,EAAE,EAAE;iBACV,CAAC,CAAC;YACH,wBAAU;YACV,kCAAe;YACf,wBAAU;YACV,kCAAe;YACf,0BAAW;YACX,8BAAa;YACb,wBAAU;YACV,8BAAa;YACb,gCAAc;SACf;QACD,WAAW,EAAE,EAAE;QACf,SAAS,EAAE;YACT;gBACE,OAAO,EAAE,sBAAe;gBACxB,QAAQ,EAAE,oCAAgB;aAC3B;SACF;KACF,CAAC;GACW,SAAS,CAAG"}
+4 -5
View File
@@ -29,11 +29,10 @@ let AuthModule = class AuthModule {
this.configService = configService;
}
async onModuleInit() {
const defaultKey = this.configService.get('MAP_API_KEY');
if (defaultKey) {
await this.authService.seedDefaultKey('Default System', 'admin@intaleq.xyz', defaultKey);
console.log('✅ Default System API Key seeded successfully');
}
const defaultKey = this.configService.get('MAP_API_KEY') || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
await this.authService.seedDefaultKey('Default System', 'admin@intaleq.xyz', defaultKey);
await this.authService.seedDefaultKey('Default Fallback', 'support@intaleq.xyz', 'intaleq_secret_2026');
console.log('✅ System API Keys seeded successfully');
}
};
exports.AuthModule = AuthModule;
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"auth.module.js","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAA8D;AAC9D,6CAAgD;AAChD,iDAA6C;AAC7C,4DAAkD;AAClD,8DAAmD;AACnD,yDAAqD;AACrD,2CAA+C;AAC/C,2DAAuD;AACvD,qEAAgE;AAChE,sEAAiE;AAY1D,IAAM,UAAU,GAAhB,MAAM,UAAU;IAEF;IACA;IAFnB,YACmB,WAAwB,EACxB,aAA4B;QAD5B,gBAAW,GAAX,WAAW,CAAa;QACxB,kBAAa,GAAb,aAAa,CAAe;IAC5C,CAAC;IAMJ,KAAK,CAAC,YAAY;QAChB,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAS,aAAa,CAAC,CAAC;QACjE,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,IAAI,CAAC,WAAW,CAAC,cAAc,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,UAAU,CAAC,CAAC;YACzF,OAAO,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;CACF,CAAA;AAjBY,gCAAU;qBAAV,UAAU;IAVtB,IAAA,eAAM,GAAE;IACR,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,uBAAa,CAAC,UAAU,CAAC,CAAC,sBAAM,EAAE,uBAAM,CAAC,CAAC;YAC1C,0BAAW;SACZ;QACD,WAAW,EAAE,CAAC,oCAAgB,CAAC;QAC/B,SAAS,EAAE,CAAC,0BAAW,EAAE,6CAAoB,EAAE,uCAAiB,CAAC;QACjE,OAAO,EAAE,CAAC,0BAAW,EAAE,6CAAoB,EAAE,uCAAiB,CAAC;KAChE,CAAC;qCAGgC,0BAAW,sBACT,sBAAa,oBAAb,sBAAa;GAHpC,UAAU,CAiBtB"}
{"version":3,"file":"auth.module.js","sourceRoot":"","sources":["../../src/auth/auth.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,2CAA8D;AAC9D,6CAAgD;AAChD,iDAA6C;AAC7C,4DAAkD;AAClD,8DAAmD;AACnD,yDAAqD;AACrD,2CAA+C;AAC/C,2DAAuD;AACvD,qEAAgE;AAChE,sEAAiE;AAY1D,IAAM,UAAU,GAAhB,MAAM,UAAU;IAEF;IACA;IAFnB,YACmB,WAAwB,EACxB,aAA4B;QAD5B,gBAAW,GAAX,WAAW,CAAa;QACxB,kBAAa,GAAb,aAAa,CAAe;IAC5C,CAAC;IAMJ,KAAK,CAAC,YAAY;QAChB,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAS,aAAa,CAAC,IAAI,kCAAkC,CAAC;QACvG,MAAM,IAAI,CAAC,WAAW,CAAC,cAAc,CAAC,gBAAgB,EAAE,mBAAmB,EAAE,UAAU,CAAC,CAAC;QACzF,MAAM,IAAI,CAAC,WAAW,CAAC,cAAc,CAAC,kBAAkB,EAAE,qBAAqB,EAAE,qBAAqB,CAAC,CAAC;QACxG,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;IACvD,CAAC;CACF,CAAA;AAhBY,gCAAU;qBAAV,UAAU;IAVtB,IAAA,eAAM,GAAE;IACR,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,uBAAa,CAAC,UAAU,CAAC,CAAC,sBAAM,EAAE,uBAAM,CAAC,CAAC;YAC1C,0BAAW;SACZ;QACD,WAAW,EAAE,CAAC,oCAAgB,CAAC;QAC/B,SAAS,EAAE,CAAC,0BAAW,EAAE,6CAAoB,EAAE,uCAAiB,CAAC;QACjE,OAAO,EAAE,CAAC,0BAAW,EAAE,6CAAoB,EAAE,uCAAiB,CAAC;KAChE,CAAC;qCAGgC,0BAAW,sBACT,sBAAa,oBAAb,sBAAa;GAHpC,UAAU,CAgBtB"}
@@ -24,9 +24,9 @@ export declare class AdministrativeLinkingService {
total_polygons: number;
districts_processed: any;
}>;
linkPlaces(country: 'jordan' | 'syria' | 'egypt'): Promise<{
linkPlaces(country: 'jordan' | 'syria' | 'egypt' | 'iraq'): Promise<{
status: string;
country: "syria" | "jordan" | "egypt";
country: "iraq" | "syria" | "jordan" | "egypt";
total_places: number;
linked_neighborhood: number;
linked_district: number;
+12 -2
View File
@@ -72,8 +72,13 @@ let AdministrativeLinkingService = AdministrativeLinkingService_1 = class Admini
this.logger.log(`Found ${elements.length} points in OSM.`);
let detectedCountry = country;
if (!detectedCountry) {
const firstLat = parseFloat(bbox.split(',')[0]);
if (firstLat > 32.3)
const parts = bbox.split(',').map(parseFloat);
const firstLat = parts[0];
const lngs = [parts[1], parts[3]].filter(n => !isNaN(n));
const maxLng = lngs.length ? Math.max(...lngs) : undefined;
if (maxLng !== undefined && maxLng > 42.5)
detectedCountry = 'iraq';
else if (firstLat > 32.3)
detectedCountry = 'syria';
else if (firstLat < 31.0)
detectedCountry = 'egypt';
@@ -106,8 +111,13 @@ let AdministrativeLinkingService = AdministrativeLinkingService_1 = class Admini
LIMIT 1
),
(
-- حدّ 50كم إلزامي: بدونه تُسنَد نقطة لا تقع داخل أي حدود إلى أقرب
-- مقاطعة في الجدول كله مهما بَعُدت. حين استوردنا العراق قبل حدوده
-- الإدارية، رُبطت أحياء بغداد الـ185 بمقاطعات سورية/أردنية على بعد
-- ~700كم. عدم الربط أفضل من ربط خاطئ صامت.
SELECT id FROM admin_boundaries ab
WHERE ab.admin_level IN (6, 8)
AND ST_DWithin(ab.geom::geography, np.geometry::geography, 50000)
ORDER BY ab.geom::geometry <-> np.geometry::geometry
LIMIT 1
)
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -51,8 +51,8 @@ __decorate([
], SearchQueryDto.prototype, "radius", void 0);
__decorate([
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsEnum)(['jordan', 'syria', 'egypt']),
(0, swagger_1.ApiPropertyOptional)({ description: 'Country filter', enum: ['jordan', 'syria', 'egypt'] }),
(0, class_validator_1.IsEnum)(['jordan', 'syria', 'egypt', 'iraq']),
(0, swagger_1.ApiPropertyOptional)({ description: 'Country filter', enum: ['jordan', 'syria', 'egypt', 'iraq'] }),
__metadata("design:type", String)
], SearchQueryDto.prototype, "country", void 0);
//# sourceMappingURL=search-query.dto.js.map
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"search-query.dto.js","sourceRoot":"","sources":["../../../src/geocoding/dto/search-query.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAAmF;AACnF,yDAAyC;AACzC,6CAAsD;AAEtD,MAAa,cAAc;IAGzB,CAAC,CAAS;IAMV,GAAG,CAAU;IAMb,GAAG,CAAU;IAQb,MAAM,GAAY,KAAK,CAAC;IAKxB,OAAO,CAAU;CAClB;AA7BD,wCA6BC;AA1BC;IAFC,IAAA,0BAAQ,GAAE;IACV,IAAA,6BAAmB,EAAC,EAAE,WAAW,EAAE,qBAAqB,EAAE,CAAC;;yCAClD;AAMV;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,6BAAmB,EAAC,EAAE,WAAW,EAAE,+BAA+B,EAAE,CAAC;;2CACzD;AAMb;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,6BAAmB,EAAC,EAAE,WAAW,EAAE,gCAAgC,EAAE,CAAC;;2CAC1D;AAQb;IANC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,qBAAG,EAAC,CAAC,CAAC;IACN,IAAA,qBAAG,EAAC,KAAK,CAAC;IACV,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,6BAAmB,EAAC,EAAE,WAAW,EAAE,4BAA4B,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;;8CAC3D;AAKxB;IAHC,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAM,EAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IACpC,IAAA,6BAAmB,EAAC,EAAE,WAAW,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;;+CAC1E"}
{"version":3,"file":"search-query.dto.js","sourceRoot":"","sources":["../../../src/geocoding/dto/search-query.dto.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qDAAmF;AACnF,yDAAyC;AACzC,6CAAsD;AAEtD,MAAa,cAAc;IAGzB,CAAC,CAAS;IAMV,GAAG,CAAU;IAMb,GAAG,CAAU;IAQb,MAAM,GAAY,KAAK,CAAC;IAKxB,OAAO,CAAU;CAClB;AA7BD,wCA6BC;AA1BC;IAFC,IAAA,0BAAQ,GAAE;IACV,IAAA,6BAAmB,EAAC,EAAE,WAAW,EAAE,qBAAqB,EAAE,CAAC;;yCAClD;AAMV;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,6BAAmB,EAAC,EAAE,WAAW,EAAE,+BAA+B,EAAE,CAAC;;2CACzD;AAMb;IAJC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,6BAAmB,EAAC,EAAE,WAAW,EAAE,gCAAgC,EAAE,CAAC;;2CAC1D;AAQb;IANC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,qBAAG,EAAC,CAAC,CAAC;IACN,IAAA,qBAAG,EAAC,KAAK,CAAC;IACV,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,MAAM,CAAC;IAClB,IAAA,6BAAmB,EAAC,EAAE,WAAW,EAAE,4BAA4B,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;;8CAC3D;AAKxB;IAHC,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAM,EAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;IAC5C,IAAA,6BAAmB,EAAC,EAAE,WAAW,EAAE,gBAAgB,EAAE,IAAI,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,CAAC;;+CAClF"}
+2 -1
View File
@@ -7,7 +7,8 @@ export declare enum CandidateStatus {
export declare enum CountryCode {
JORDAN = "JORDAN",
SYRIA = "SYRIA",
EGYPT = "EGYPT"
EGYPT = "EGYPT",
IRAQ = "IRAQ"
}
export declare class MapCandidate extends BasePlace {
status: CandidateStatus;
@@ -23,6 +23,7 @@ var CountryCode;
CountryCode["JORDAN"] = "JORDAN";
CountryCode["SYRIA"] = "SYRIA";
CountryCode["EGYPT"] = "EGYPT";
CountryCode["IRAQ"] = "IRAQ";
})(CountryCode || (exports.CountryCode = CountryCode = {}));
let MapCandidate = class MapCandidate extends base_place_entity_1.BasePlace {
status;
@@ -1 +1 @@
{"version":3,"file":"map-candidate.entity.js","sourceRoot":"","sources":["../../../src/geocoding/entities/map-candidate.entity.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qCAAgD;AAChD,2DAAgD;AAEhD,IAAY,eAIX;AAJD,WAAY,eAAe;IACzB,sCAAmB,CAAA;IACnB,wCAAqB,CAAA;IACrB,wCAAqB,CAAA;AACvB,CAAC,EAJW,eAAe,+BAAf,eAAe,QAI1B;AAED,IAAY,WAIX;AAJD,WAAY,WAAW;IACrB,gCAAiB,CAAA;IACjB,8BAAe,CAAA;IACf,8BAAe,CAAA;AACjB,CAAC,EAJW,WAAW,2BAAX,WAAW,QAItB;AAGM,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,6BAAS;IAOzC,MAAM,CAAkB;IAIxB,WAAW,CAAS;IAGpB,eAAe,CAAS;IAQxB,OAAO,CAAc;IAGrB,QAAQ,CAAM;IAGd,aAAa,CAAS;IAGtB,gBAAgB,CAAS;IAGzB,qBAAqB,CAAM;CAC5B,CAAA;AAnCY,oCAAY;AAOvB;IANC,IAAA,gBAAM,EAAC;QACN,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,eAAe,CAAC,OAAO;KACjC,CAAC;IACD,IAAA,eAAK,GAAE;;4CACgB;AAIxB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,eAAK,GAAE;;iDACY;AAGpB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;qDACjB;AAQxB;IANC,IAAA,gBAAM,EAAC;QACN,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,WAAW,CAAC,MAAM;KAC5B,CAAC;IACD,IAAA,eAAK,GAAE;;6CACa;AAGrB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;8CAC5B;AAGd;IADC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;mDACL;AAGtB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;;sDACX;AAGzB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;2DACf;uBAlChB,YAAY;IADxB,IAAA,gBAAM,EAAC,gBAAgB,CAAC;GACZ,YAAY,CAmCxB"}
{"version":3,"file":"map-candidate.entity.js","sourceRoot":"","sources":["../../../src/geocoding/entities/map-candidate.entity.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qCAAgD;AAChD,2DAAgD;AAEhD,IAAY,eAIX;AAJD,WAAY,eAAe;IACzB,sCAAmB,CAAA;IACnB,wCAAqB,CAAA;IACrB,wCAAqB,CAAA;AACvB,CAAC,EAJW,eAAe,+BAAf,eAAe,QAI1B;AAED,IAAY,WAKX;AALD,WAAY,WAAW;IACrB,gCAAiB,CAAA;IACjB,8BAAe,CAAA;IACf,8BAAe,CAAA;IACf,4BAAa,CAAA;AACf,CAAC,EALW,WAAW,2BAAX,WAAW,QAKtB;AAGM,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,6BAAS;IAOzC,MAAM,CAAkB;IAIxB,WAAW,CAAS;IAGpB,eAAe,CAAS;IAQxB,OAAO,CAAc;IAGrB,QAAQ,CAAM;IAGd,aAAa,CAAS;IAGtB,gBAAgB,CAAS;IAGzB,qBAAqB,CAAM;CAC5B,CAAA;AAnCY,oCAAY;AAOvB;IANC,IAAA,gBAAM,EAAC;QACN,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,eAAe,CAAC,OAAO;KACjC,CAAC;IACD,IAAA,eAAK,GAAE;;4CACgB;AAIxB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,eAAK,GAAE;;iDACY;AAGpB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;qDACjB;AAQxB;IANC,IAAA,gBAAM,EAAC;QACN,IAAI,EAAE,MAAM;QACZ,IAAI,EAAE,WAAW;QACjB,OAAO,EAAE,WAAW,CAAC,MAAM;KAC5B,CAAC;IACD,IAAA,eAAK,GAAE;;6CACa;AAGrB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;8CAC5B;AAGd;IADC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;mDACL;AAGtB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;;sDACX;AAGzB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;2DACf;uBAlChB,YAAY;IADxB,IAAA,gBAAM,EAAC,gBAAgB,CAAC;GACZ,YAAY,CAmCxB"}
@@ -0,0 +1,3 @@
import { BasePlace } from './base-place.entity';
export declare class PlaceIraq extends BasePlace {
}
+18
View File
@@ -0,0 +1,18 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PlaceIraq = void 0;
const typeorm_1 = require("typeorm");
const base_place_entity_1 = require("./base-place.entity");
let PlaceIraq = class PlaceIraq extends base_place_entity_1.BasePlace {
};
exports.PlaceIraq = PlaceIraq;
exports.PlaceIraq = PlaceIraq = __decorate([
(0, typeorm_1.Entity)('places_iraq')
], PlaceIraq);
//# sourceMappingURL=place-iraq.entity.js.map
@@ -0,0 +1 @@
{"version":3,"file":"place-iraq.entity.js","sourceRoot":"","sources":["../../../src/geocoding/entities/place-iraq.entity.ts"],"names":[],"mappings":";;;;;;;;;AAAA,qCAAiC;AACjC,2DAAgD;AAGzC,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,6BAAS;CAAG,CAAA;AAA9B,8BAAS;oBAAT,SAAS;IADrB,IAAA,gBAAM,EAAC,aAAa,CAAC;GACT,SAAS,CAAqB"}
+10 -2
View File
@@ -30,7 +30,7 @@ let GeocodingInitService = GeocodingInitService_1 = class GeocodingInitService {
try {
await this.repo.query('CREATE EXTENSION IF NOT EXISTS postgis;');
await this.repo.query('CREATE EXTENSION IF NOT EXISTS pg_trgm;');
const tables = ['places_jordan', 'places_syria', 'places_egypt'];
const tables = ['places_jordan', 'places_syria', 'places_egypt', 'places_iraq'];
const columnMapping = [
{ old: 'admin_level4_id', new: 'governorate_id' },
{ old: 'admin_level6_id', new: 'district_id' },
@@ -84,12 +84,20 @@ let GeocodingInitService = GeocodingInitService_1 = class GeocodingInitService {
CREATE TRIGGER trg_sync_place_location_egypt
BEFORE INSERT OR UPDATE ON places_egypt
FOR EACH ROW EXECUTE FUNCTION sync_place_location();
`);
await this.repo.query(`
DROP TRIGGER IF EXISTS trg_sync_place_location_iraq ON places_iraq;
CREATE TRIGGER trg_sync_place_location_iraq
BEFORE INSERT OR UPDATE ON places_iraq
FOR EACH ROW EXECUTE FUNCTION sync_place_location();
`);
await this.repo.query('CREATE INDEX IF NOT EXISTS idx_places_jordan_location ON places_jordan USING gist (location);');
await this.repo.query('CREATE INDEX IF NOT EXISTS idx_places_egypt_location ON places_egypt USING gist (location);');
await this.repo.query('CREATE INDEX IF NOT EXISTS idx_places_iraq_location ON places_iraq USING gist (location);');
await this.repo.query('CREATE INDEX IF NOT EXISTS idx_places_jordan_names_trgm ON places_jordan USING gist (name_ar gist_trgm_ops, name_en gist_trgm_ops);');
await this.repo.query('CREATE INDEX IF NOT EXISTS idx_places_egypt_names_trgm ON places_egypt USING gist (name_ar gist_trgm_ops, name_en gist_trgm_ops);');
this.logger.log('Geocoding database triggers and optimized indexes initialized for Syria, Jordan, and Egypt.');
await this.repo.query('CREATE INDEX IF NOT EXISTS idx_places_iraq_names_trgm ON places_iraq USING gist (name_ar gist_trgm_ops, name_en gist_trgm_ops);');
this.logger.log('Geocoding database triggers and optimized indexes initialized for Syria, Jordan, Egypt, and Iraq.');
}
catch (err) {
this.logger.error('Failed to initialize database geocoding triggers:', err);
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"geocoding-init.service.js","sourceRoot":"","sources":["../../src/geocoding/geocoding-init.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2CAAkE;AAClE,6CAAmD;AACnD,qCAAqC;AACrC,sEAA2D;AAGpD,IAAM,oBAAoB,4BAA1B,MAAM,oBAAoB;IAKZ;IAJF,MAAM,GAAG,IAAI,eAAM,CAAC,sBAAoB,CAAC,IAAI,CAAC,CAAC;IAEhE,YAEmB,IAA4B;QAA5B,SAAI,GAAJ,IAAI,CAAwB;IAC5C,CAAC;IAEJ,KAAK,CAAC,YAAY;QAChB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;QACnE,IAAI,CAAC;YAEH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;YACjE,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;YAGjE,MAAM,MAAM,GAAG,CAAC,eAAe,EAAE,cAAc,EAAE,cAAc,CAAC,CAAC;YACjE,MAAM,aAAa,GAAG;gBACpB,EAAE,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,EAAE;gBACjD,EAAE,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,aAAa,EAAE;gBAC9C,EAAE,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,iBAAiB,EAAE;gBAClD,EAAE,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,iBAAiB,EAAE;aACpD,CAAC;YAEF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;oBACpC,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;wFAGwD,KAAK,wBAAwB,OAAO,CAAC,GAAG;8BAClG,KAAK,kBAAkB,OAAO,CAAC,GAAG,OAAO,OAAO,CAAC,GAAG;;;WAGvE,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YAGD,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;OAiBrB,CAAC,CAAC;YAEH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;;;OAKrB,CAAC,CAAC;YAGH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;;;OAKrB,CAAC,CAAC;YAEH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;;;OAKrB,CAAC,CAAC;YAGH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,+FAA+F,CAAC,CAAC;YACvH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,6FAA6F,CAAC,CAAC;YAGrH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,qIAAqI,CAAC,CAAC;YAC7J,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,mIAAmI,CAAC,CAAC;YAE3J,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,6FAA6F,CAAC,CAAC;QACjH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,mDAAmD,EAAE,GAAG,CAAC,CAAC;QAC9E,CAAC;IACH,CAAC;CACF,CAAA;AA5FY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,mBAAU,GAAE;IAKR,WAAA,IAAA,0BAAgB,EAAC,+BAAU,CAAC,CAAA;yDACN,oBAAU,oBAAV,oBAAU;GALxB,oBAAoB,CA4FhC"}
{"version":3,"file":"geocoding-init.service.js","sourceRoot":"","sources":["../../src/geocoding/geocoding-init.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,2CAAkE;AAClE,6CAAmD;AACnD,qCAAqC;AACrC,sEAA2D;AAGpD,IAAM,oBAAoB,4BAA1B,MAAM,oBAAoB;IAKZ;IAJF,MAAM,GAAG,IAAI,eAAM,CAAC,sBAAoB,CAAC,IAAI,CAAC,CAAC;IAEhE,YAEmB,IAA4B;QAA5B,SAAI,GAAJ,IAAI,CAAwB;IAC5C,CAAC;IAEJ,KAAK,CAAC,YAAY;QAChB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;QACnE,IAAI,CAAC;YAEH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;YACjE,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;YAGjE,MAAM,MAAM,GAAG,CAAC,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,CAAC,CAAC;YAChF,MAAM,aAAa,GAAG;gBACpB,EAAE,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,gBAAgB,EAAE;gBACjD,EAAE,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,aAAa,EAAE;gBAC9C,EAAE,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,iBAAiB,EAAE;gBAClD,EAAE,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,iBAAiB,EAAE;aACpD,CAAC;YAEF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;gBAC3B,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;oBACpC,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;wFAGwD,KAAK,wBAAwB,OAAO,CAAC,GAAG;8BAClG,KAAK,kBAAkB,OAAO,CAAC,GAAG,OAAO,OAAO,CAAC,GAAG;;;WAGvE,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YAGD,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;;;;;;;;;;;;;;;OAiBrB,CAAC,CAAC;YAEH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;;;OAKrB,CAAC,CAAC;YAGH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;;;OAKrB,CAAC,CAAC;YAEH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;;;OAKrB,CAAC,CAAC;YAEH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC;;;;;OAKrB,CAAC,CAAC;YAGH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,+FAA+F,CAAC,CAAC;YACvH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,6FAA6F,CAAC,CAAC;YACrH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,2FAA2F,CAAC,CAAC;YAGnH,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,qIAAqI,CAAC,CAAC;YAC7J,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,mIAAmI,CAAC,CAAC;YAC3J,MAAM,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,iIAAiI,CAAC,CAAC;YAEzJ,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,mGAAmG,CAAC,CAAC;QACvH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,mDAAmD,EAAE,GAAG,CAAC,CAAC;QAC9E,CAAC;IACH,CAAC;CACF,CAAA;AArGY,oDAAoB;+BAApB,oBAAoB;IADhC,IAAA,mBAAU,GAAE;IAKR,WAAA,IAAA,0BAAgB,EAAC,+BAAU,CAAC,CAAA;yDACN,oBAAU,oBAAV,oBAAU;GALxB,oBAAoB,CAqGhC"}
+9 -2
View File
@@ -49,6 +49,13 @@ export declare class GeocodingController {
processed: number;
created: number;
updated: number;
failed: number;
errors: {
index: number;
name?: string;
reason: string;
}[];
errorsTruncated: boolean;
}>;
getPlaces(limit?: string): Promise<any[]>;
getGeoJSON(): Promise<{
@@ -87,9 +94,9 @@ export declare class GeocodingController {
total_polygons: number;
districts_processed: any;
}>;
linkPlaces(country: 'jordan' | 'syria' | 'egypt'): Promise<{
linkPlaces(country: 'jordan' | 'syria' | 'egypt' | 'iraq'): Promise<{
status: string;
country: "syria" | "jordan" | "egypt";
country: "iraq" | "syria" | "jordan" | "egypt";
total_places: number;
linked_neighborhood: number;
linked_district: number;
+4 -4
View File
@@ -135,7 +135,7 @@ __decorate([
(0, swagger_1.ApiOperation)({ summary: 'Delete a place by name or ID' }),
(0, swagger_1.ApiQuery)({ name: 'name', required: false }),
(0, swagger_1.ApiQuery)({ name: 'id', required: false, type: Number }),
(0, swagger_1.ApiQuery)({ name: 'country', required: true, enum: ['jordan', 'syria', 'egypt'] }),
(0, swagger_1.ApiQuery)({ name: 'country', required: true, enum: ['jordan', 'syria', 'egypt', 'iraq'] }),
__param(0, (0, common_1.Query)('country')),
__param(1, (0, common_1.Query)('name')),
__param(2, (0, common_1.Query)('id')),
@@ -200,7 +200,7 @@ __decorate([
(0, common_1.UseGuards)(admin_guard_1.AdminGuard),
(0, swagger_1.ApiOperation)({ summary: 'Sync neighborhood points from OSM for a bbox' }),
(0, swagger_1.ApiQuery)({ name: 'bbox', required: false }),
(0, swagger_1.ApiQuery)({ name: 'country', required: false, enum: ['jordan', 'syria', 'egypt'] }),
(0, swagger_1.ApiQuery)({ name: 'country', required: false, enum: ['jordan', 'syria', 'egypt', 'iraq'] }),
__param(0, (0, common_1.Query)('bbox')),
__param(1, (0, common_1.Query)('country')),
__metadata("design:type", Function),
@@ -211,7 +211,7 @@ __decorate([
(0, common_1.Post)('admin/generate-voronoi'),
(0, common_1.UseGuards)(admin_guard_1.AdminGuard),
(0, swagger_1.ApiOperation)({ summary: 'Generate Voronoi polygons for neighborhoods' }),
(0, swagger_1.ApiQuery)({ name: 'country', required: false, enum: ['jordan', 'syria', 'egypt'] }),
(0, swagger_1.ApiQuery)({ name: 'country', required: false, enum: ['jordan', 'syria', 'egypt', 'iraq'] }),
__param(0, (0, common_1.Query)('country')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
@@ -221,7 +221,7 @@ __decorate([
(0, common_1.Post)('admin/link-places'),
(0, common_1.UseGuards)(admin_guard_1.AdminGuard),
(0, swagger_1.ApiOperation)({ summary: 'Link places to administrative hierarchy' }),
(0, swagger_1.ApiQuery)({ name: 'country', required: true, enum: ['jordan', 'syria', 'egypt'] }),
(0, swagger_1.ApiQuery)({ name: 'country', required: true, enum: ['jordan', 'syria', 'egypt', 'iraq'] }),
__param(0, (0, common_1.Query)('country')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
File diff suppressed because one or more lines are too long
+2
View File
@@ -48,6 +48,7 @@ const geocoding_controller_1 = require("./geocoding.controller");
const place_syria_entity_1 = require("./entities/place-syria.entity");
const place_jordan_entity_1 = require("./entities/place-jordan.entity");
const place_egypt_entity_1 = require("./entities/place-egypt.entity");
const place_iraq_entity_1 = require("./entities/place-iraq.entity");
const osm_area_entity_1 = require("./entities/osm-area.entity");
const osm_point_with_area_entity_1 = require("./entities/osm-point-with-area.entity");
const admin_boundary_entity_1 = require("./entities/admin-boundary.entity");
@@ -72,6 +73,7 @@ exports.GeocodingModule = GeocodingModule = __decorate([
place_syria_entity_1.PlaceSyria,
place_jordan_entity_1.PlaceJordan,
place_egypt_entity_1.PlaceEgypt,
place_iraq_entity_1.PlaceIraq,
osm_area_entity_1.OsmArea,
osm_point_with_area_entity_1.OsmPointWithArea,
admin_boundary_entity_1.AdminBoundary,
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"geocoding.module.js","sourceRoot":"","sources":["../../src/geocoding/geocoding.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAwC;AACxC,6CAAgD;AAChD,2DAAuD;AACvD,qEAAgE;AAChE,iEAA6D;AAC7D,sEAA2D;AAC3D,wEAA6D;AAC7D,sEAA2D;AAC3D,gEAAqD;AACrD,sFAAyE;AACzE,4EAAiE;AACjE,yEAAoE;AACpE,uEAAkE;AAClE,qFAAgF;AAChF,oFAAyE;AACzE,wFAA6E;AAC7E,0EAA+D;AAC/D,qEAAgE;AAChE,2EAAsE;AACtE,yDAAoD;AACpD,sEAAwD;AACxD,mEAA8D;AAkCvD,IAAM,eAAe,GAArB,MAAM,eAAe;CAAG,CAAA;AAAlB,0CAAe;0BAAf,eAAe;IAhC3B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,uBAAa,CAAC,UAAU,CAAC;gBACvB,+BAAU;gBACV,iCAAW;gBACX,+BAAU;gBACV,yBAAO;gBACP,6CAAgB;gBAChB,qCAAa;gBACb,6CAAiB;gBACjB,iDAAmB;gBACnB,mCAAY;aACb,CAAC;YACF,2BAAW,CAAC,QAAQ,CAAC;gBACnB,KAAK,EAAE,UAAU;gBACjB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,WAAW;gBAC3C,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,MAAM,EAAE,EAAE,CAAC;gBACpD,GAAG,EAAE,IAAI;aACV,CAAC;SACH;QACD,WAAW,EAAE,CAAC,0CAAmB,EAAE,mDAAuB,CAAC;QAC3D,SAAS,EAAE;YACT,oCAAgB;YAChB,6CAAoB;YACpB,iDAAsB;YACtB,+CAAqB;YACrB,6DAA4B;YAC5B,6CAAoB;YACpB,2CAAmB;SACpB;QACD,OAAO,EAAE,CAAC,oCAAgB,EAAE,6CAAoB,CAAC;KAClD,CAAC;GACW,eAAe,CAAG"}
{"version":3,"file":"geocoding.module.js","sourceRoot":"","sources":["../../src/geocoding/geocoding.module.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAAwC;AACxC,6CAAgD;AAChD,2DAAuD;AACvD,qEAAgE;AAChE,iEAA6D;AAC7D,sEAA2D;AAC3D,wEAA6D;AAC7D,sEAA2D;AAC3D,oEAAyD;AACzD,gEAAqD;AACrD,sFAAyE;AACzE,4EAAiE;AACjE,yEAAoE;AACpE,uEAAkE;AAClE,qFAAgF;AAChF,oFAAyE;AACzE,wFAA6E;AAC7E,0EAA+D;AAC/D,qEAAgE;AAChE,2EAAsE;AACtE,yDAAoD;AACpD,sEAAwD;AACxD,mEAA8D;AAmCvD,IAAM,eAAe,GAArB,MAAM,eAAe;CAAG,CAAA;AAAlB,0CAAe;0BAAf,eAAe;IAjC3B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,uBAAa,CAAC,UAAU,CAAC;gBACvB,+BAAU;gBACV,iCAAW;gBACX,+BAAU;gBACV,6BAAS;gBACT,yBAAO;gBACP,6CAAgB;gBAChB,qCAAa;gBACb,6CAAiB;gBACjB,iDAAmB;gBACnB,mCAAY;aACb,CAAC;YACF,2BAAW,CAAC,QAAQ,CAAC;gBACnB,KAAK,EAAE,UAAU;gBACjB,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,WAAW;gBAC3C,IAAI,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,MAAM,EAAE,EAAE,CAAC;gBACpD,GAAG,EAAE,IAAI;aACV,CAAC;SACH;QACD,WAAW,EAAE,CAAC,0CAAmB,EAAE,mDAAuB,CAAC;QAC3D,SAAS,EAAE;YACT,oCAAgB;YAChB,6CAAoB;YACpB,iDAAsB;YACtB,+CAAqB;YACrB,6DAA4B;YAC5B,6CAAoB;YACpB,2CAAmB;SACpB;QACD,OAAO,EAAE,CAAC,oCAAgB,EAAE,6CAAoB,CAAC;KAClD,CAAC;GACW,eAAe,CAAG"}
+12 -1
View File
@@ -3,6 +3,7 @@ import type { Cache } from 'cache-manager';
import { PlaceSyria } from './entities/place-syria.entity';
import { PlaceJordan } from './entities/place-jordan.entity';
import { PlaceEgypt } from './entities/place-egypt.entity';
import { PlaceIraq } from './entities/place-iraq.entity';
import { BasePlace } from './entities/base-place.entity';
import { OsmArea } from './entities/osm-area.entity';
import { OsmPointWithArea } from './entities/osm-point-with-area.entity';
@@ -10,12 +11,13 @@ export declare class GeocodingService {
private placesSyriaRepository;
private placesJordanRepository;
private placesEgyptRepository;
private placesIraqRepository;
private osmAreasRepository;
private osmPointsRepository;
private cacheManager;
private readonly logger;
private readonly DB_TIMEOUT_MS;
constructor(placesSyriaRepository: Repository<PlaceSyria>, placesJordanRepository: Repository<PlaceJordan>, placesEgyptRepository: Repository<PlaceEgypt>, osmAreasRepository: Repository<OsmArea>, osmPointsRepository: Repository<OsmPointWithArea>, cacheManager: Cache);
constructor(placesSyriaRepository: Repository<PlaceSyria>, placesJordanRepository: Repository<PlaceJordan>, placesEgyptRepository: Repository<PlaceEgypt>, placesIraqRepository: Repository<PlaceIraq>, osmAreasRepository: Repository<OsmArea>, osmPointsRepository: Repository<OsmPointWithArea>, cacheManager: Cache);
private getRepositoryForCoords;
private identifyRegion;
private getTableNameForRepo;
@@ -42,7 +44,9 @@ export declare class GeocodingService {
results: any;
source?: undefined;
}>;
private findIraqiAddress;
reverseGeocode(lat: number, lng: number): Promise<any[]>;
private extractCoords;
addPlace(data: Partial<BasePlace>): Promise<any>;
upsertPlace(data: Partial<BasePlace>): Promise<{
id: any;
@@ -53,6 +57,13 @@ export declare class GeocodingService {
processed: number;
created: number;
updated: number;
failed: number;
errors: {
index: number;
name?: string;
reason: string;
}[];
errorsTruncated: boolean;
}>;
getRecentPlaces(limit?: number): Promise<any[]>;
getAllPlacesGeoJSON(): Promise<{
+127 -16
View File
@@ -12,7 +12,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var GeocodingService_1;
var _a, _b, _c, _d, _e;
var _a, _b, _c, _d, _e, _f;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GeocodingService = void 0;
const common_1 = require("@nestjs/common");
@@ -22,26 +22,32 @@ const cache_manager_1 = require("@nestjs/cache-manager");
const place_syria_entity_1 = require("./entities/place-syria.entity");
const place_jordan_entity_1 = require("./entities/place-jordan.entity");
const place_egypt_entity_1 = require("./entities/place-egypt.entity");
const place_iraq_entity_1 = require("./entities/place-iraq.entity");
const osm_area_entity_1 = require("./entities/osm-area.entity");
const osm_point_with_area_entity_1 = require("./entities/osm-point-with-area.entity");
let GeocodingService = GeocodingService_1 = class GeocodingService {
placesSyriaRepository;
placesJordanRepository;
placesEgyptRepository;
placesIraqRepository;
osmAreasRepository;
osmPointsRepository;
cacheManager;
logger = new common_1.Logger(GeocodingService_1.name);
DB_TIMEOUT_MS = 1100;
constructor(placesSyriaRepository, placesJordanRepository, placesEgyptRepository, osmAreasRepository, osmPointsRepository, cacheManager) {
constructor(placesSyriaRepository, placesJordanRepository, placesEgyptRepository, placesIraqRepository, osmAreasRepository, osmPointsRepository, cacheManager) {
this.placesSyriaRepository = placesSyriaRepository;
this.placesJordanRepository = placesJordanRepository;
this.placesEgyptRepository = placesEgyptRepository;
this.placesIraqRepository = placesIraqRepository;
this.osmAreasRepository = osmAreasRepository;
this.osmPointsRepository = osmPointsRepository;
this.cacheManager = cacheManager;
}
getRepositoryForCoords(lat, lng) {
if (lat >= 29 && lat <= 37.5 && lng >= 38.7 && lng <= 48.8) {
return this.placesIraqRepository;
}
if (lat >= 29 && lat <= 37.5 && lng >= 34.5 && lng <= 42.5) {
if (lat > 32.5 && lng > 35.8)
return this.placesSyriaRepository;
@@ -55,6 +61,9 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
identifyRegion(lat, lng) {
if (lat === undefined || lng === undefined)
return undefined;
if (lat >= 29 && lat <= 37.5 && lng >= 38.7 && lng <= 48.8) {
return 'iraq';
}
if (lat >= 29 && lat <= 37.5 && lng >= 34.5 && lng <= 42.5) {
if (lat > 32.5 && lng > 35.8)
return 'syria';
@@ -70,6 +79,8 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
return 'places_jordan';
if (repo === this.placesEgyptRepository)
return 'places_egypt';
if (repo === this.placesIraqRepository)
return 'places_iraq';
return 'places_syria';
}
async searchPlaces(query, lat, lon, radius = 20000, country) {
@@ -100,7 +111,7 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
queryParams.push(lat, lon, radius);
}
let regionCondition = '';
if (targetRegion && ['syria', 'jordan', 'egypt'].includes(targetRegion)) {
if (targetRegion && ['syria', 'jordan', 'egypt', 'iraq'].includes(targetRegion)) {
regionCondition = `AND (region = '${targetRegion}' OR region = 'global')`;
}
const sqlQuery = `
@@ -227,6 +238,8 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
getRepoByTableName(tableName) {
if (tableName === 'places_egypt')
return this.placesEgyptRepository;
if (tableName === 'places_iraq')
return this.placesIraqRepository;
if (tableName === 'places_jordan')
return this.placesJordanRepository;
return this.placesSyriaRepository;
@@ -263,7 +276,7 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
const normalizedQuery = normalizedQueryRes?.nq || cleanQuery.toLowerCase();
let queryParams = [`${normalizedQuery}%`];
let regionCondition = '';
if (targetRegion && ['syria', 'jordan', 'egypt'].includes(targetRegion)) {
if (targetRegion && ['syria', 'jordan', 'egypt', 'iraq'].includes(targetRegion)) {
regionCondition = `AND (region = '${targetRegion}' OR region = 'global')`;
}
const sqlQuery = `
@@ -297,10 +310,44 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
return { results: [] };
}
}
async findIraqiAddress(lat, lng) {
const MAX_DISTANCE_M = 150;
try {
const rows = await this.osmPointsRepository.query(`
WITH p AS (
SELECT ST_Transform(ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326), 3857) AS g3857,
ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)::geography AS geog
)
SELECT l.name,
ST_Distance(ST_Transform(l.way, 4326)::geography, p.geog) AS distance
FROM planet_osm_line l, p
WHERE l.name ~ '^[0-9]{2,4}-[0-9]{1,3}$'
ORDER BY l.way <-> p.g3857
LIMIT 1
`, [lng, lat]);
const row = rows?.[0];
if (!row || Number(row.distance) > MAX_DISTANCE_M)
return null;
const [mahalla, zuqaq] = String(row.name).split('-');
return {
mahalla,
zuqaq,
distance: Number(row.distance),
text: `محلة ${mahalla}، زقاق ${zuqaq}`,
};
}
catch (e) {
this.logger.warn(`Iraqi address lookup failed: ${e.message}`);
return null;
}
}
async reverseGeocode(lat, lng) {
try {
const repo = this.getRepositoryForCoords(lat, lng);
const tableName = this.getTableNameForRepo(repo);
const iraqiAddressPromise = this.identifyRegion(lat, lng) === 'iraq'
? this.findIraqiAddress(lat, lng)
: Promise.resolve(null);
const queryPromises = [];
queryPromises.push(repo.query(`
SELECT
@@ -357,7 +404,20 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
WHERE location IS NOT NULL AND (names->>'primary' IS NOT NULL OR names->>'common' IS NOT NULL)
ORDER BY location <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 5
`, [lng, lat]));
const results = await Promise.allSettled(queryPromises);
queryPromises.push(repo.query(`
SELECT
id::text, COALESCE(name, 'Street') as name, COALESCE(name, 'طريق معتمد') as name_ar, 'street' as category,
'' as neighbourhood, '' as district, '' as governorate,
ST_Y(ST_Centroid(geometry::geometry))::text as latitude, ST_X(ST_Centroid(geometry::geometry))::text as longitude, '' as address, 'approved_road' as source,
ST_DistanceSphere(geometry::geometry, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) as distance
FROM approved_roads
WHERE geometry IS NOT NULL AND name IS NOT NULL AND trim(name) != ''
ORDER BY geometry::geometry <-> ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326) ASC LIMIT 5
`, [lng, lat]).catch(() => []));
const [results, iraqiAddress] = await Promise.all([
Promise.allSettled(queryPromises),
iraqiAddressPromise,
]);
let allResults = [];
results.forEach(res => {
if (res.status === 'fulfilled' && res.value)
@@ -368,7 +428,17 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
.slice(0, 5)
.map(r => {
const distance = Number(r.distance);
const fullAddressParts = [r.name_ar || r.name, r.address, r.neighbourhood, r.district, r.governorate].filter(Boolean);
const ZUQAQ_NAME_RE = /^[0-9]{2,4}-[0-9]{1,3}$/;
const rawName = r.name_ar || r.name;
const nameIsZuqaq = typeof rawName === 'string' && ZUQAQ_NAME_RE.test(rawName);
const fullAddressParts = [
nameIsZuqaq ? null : rawName,
r.address,
iraqiAddress?.text,
r.neighbourhood,
r.district,
r.governorate,
].filter(Boolean);
let humanReadable = r.name_ar || r.name;
if (distance <= 20) {
humanReadable = r.name_ar || r.name;
@@ -382,8 +452,14 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
const districtPart = r.neighbourhood || r.district || '';
humanReadable = [streetPart, districtPart].filter(Boolean).join('، ');
}
if (iraqiAddress) {
const landmark = distance <= 70 && !nameIsZuqaq ? rawName : null;
humanReadable = [iraqiAddress.text, landmark].filter(Boolean).join(' — ');
}
return {
...r,
mahalla: iraqiAddress?.mahalla,
zuqaq: iraqiAddress?.zuqaq,
latitude: parseFloat(r.latitude),
longitude: parseFloat(r.longitude),
human_readable_address: humanReadable,
@@ -396,9 +472,20 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
return [];
}
}
extractCoords(data) {
const lat = Number(data?.lat ?? data?.latitude);
const lng = Number(data?.lng ?? data?.longitude);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
throw new common_1.HttpException('Invalid coordinates: provide numeric lat/lng (or latitude/longitude)', common_1.HttpStatus.BAD_REQUEST);
}
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
throw new common_1.HttpException(`Coordinates out of range: lat=${lat}, lng=${lng}`, common_1.HttpStatus.BAD_REQUEST);
}
return { lat, lng };
}
async addPlace(data) {
try {
const lat = Number(data.latitude), lng = Number(data.longitude);
const { lat, lng } = this.extractCoords(data);
const repo = this.getRepositoryForCoords(lat, lng), tableName = this.getTableNameForRepo(repo);
const newPlace = repo.create({ ...data, latitude: lat, longitude: lng, created_at: new Date(), location: { type: 'Point', coordinates: [lng, lat] } });
const savedPlace = await repo.save(newPlace);
@@ -406,12 +493,14 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
return { ...savedPlace, latitude: lat, longitude: lng, location: `POINT(${lng} ${lat})` };
}
catch (error) {
if (error instanceof common_1.HttpException)
throw error;
throw new common_1.HttpException(error.message, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async upsertPlace(data) {
try {
const lat = Number(data.latitude), lng = Number(data.longitude);
const { lat, lng } = this.extractCoords(data);
const repo = this.getRepositoryForCoords(lat, lng), tableName = this.getTableNameForRepo(repo);
const existing = await repo.query(`SELECT id, name FROM ${tableName} WHERE ST_DistanceSphere(location, ST_SetSRID(ST_MakePoint($1::float, $2::float), 4326)) < 15 LIMIT 1`, [lng, lat]);
if (existing && existing.length > 0) {
@@ -427,20 +516,39 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
}
async upsertBatch(places) {
const results = [];
for (const place of places) {
const errors = [];
for (const [index, place] of places.entries()) {
try {
const r = await this.addPlace(place);
results.push({ id: r.id, action: 'created' });
}
catch (e) { }
catch (e) {
if (errors.length < 50) {
errors.push({
index,
name: place?.name,
reason: e instanceof common_1.HttpException ? e.message : (e?.message ?? 'unknown'),
});
}
}
}
return { total: places.length, processed: results.length, created: results.length, updated: 0 };
const failed = places.length - results.length;
return {
total: places.length,
processed: results.length,
created: results.length,
updated: 0,
failed,
errors,
errorsTruncated: failed > errors.length,
};
}
async getRecentPlaces(limit = 50) {
const s = await this.placesSyriaRepository.find({ order: { created_at: 'DESC' }, take: limit });
const j = await this.placesJordanRepository.find({ order: { created_at: 'DESC' }, take: limit });
const e = await this.placesEgyptRepository.find({ order: { created_at: 'DESC' }, take: limit });
return [...s, ...j, ...e].sort((a, b) => b.created_at.getTime() - a.created_at.getTime()).slice(0, limit);
const i = await this.placesIraqRepository.find({ order: { created_at: 'DESC' }, take: limit });
return [...s, ...j, ...e, ...i].sort((a, b) => b.created_at.getTime() - a.created_at.getTime()).slice(0, limit);
}
async getAllPlacesGeoJSON() {
try {
@@ -448,6 +556,7 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
SELECT id::text, name_ar as name, category, latitude, longitude, address, 'user' as region FROM places_syria
UNION ALL SELECT id::text, name_ar as name, category, latitude, longitude, address, 'user' as region FROM places_jordan
UNION ALL SELECT id::text, name_ar as name, category, latitude, longitude, address, 'user' as region FROM places_egypt
UNION ALL SELECT id::text, name_ar as name, category, latitude, longitude, address, 'user' as region FROM places_iraq
UNION ALL SELECT id::text, COALESCE(names->>'primary', names->>'common', 'Building') as name, 'building' as category, ST_Y(ST_Centroid(location)) as latitude, ST_X(ST_Centroid(location)) as longitude, '' as address, 'overture' as region FROM overture_building WHERE names->>'primary' IS NOT NULL LIMIT 500
UNION ALL SELECT id::text, COALESCE(names->>'primary', names->>'common', 'Street') as name, 'street' as category, ST_Y(ST_Centroid(location)) as latitude, ST_X(ST_Centroid(location)) as longitude, '' as address, 'overture' as region FROM overture_segment WHERE names->>'primary' IS NOT NULL LIMIT 500
UNION ALL SELECT id::text, COALESCE(names->>'primary', names->>'common', 'Place') as name, 'place' as category, ST_Y(ST_Centroid(location)) as latitude, ST_X(ST_Centroid(location)) as longitude, '' as address, 'overture' as region FROM overture_place WHERE names->>'primary' IS NOT NULL LIMIT 500
@@ -478,6 +587,7 @@ let GeocodingService = GeocodingService_1 = class GeocodingService {
case 'syria': return this.placesSyriaRepository;
case 'jordan': return this.placesJordanRepository;
case 'egypt': return this.placesEgyptRepository;
case 'iraq': return this.placesIraqRepository;
default: throw new common_1.HttpException('Invalid country: ' + country, common_1.HttpStatus.BAD_REQUEST);
}
}
@@ -488,9 +598,10 @@ exports.GeocodingService = GeocodingService = GeocodingService_1 = __decorate([
__param(0, (0, typeorm_1.InjectRepository)(place_syria_entity_1.PlaceSyria)),
__param(1, (0, typeorm_1.InjectRepository)(place_jordan_entity_1.PlaceJordan)),
__param(2, (0, typeorm_1.InjectRepository)(place_egypt_entity_1.PlaceEgypt)),
__param(3, (0, typeorm_1.InjectRepository)(osm_area_entity_1.OsmArea)),
__param(4, (0, typeorm_1.InjectRepository)(osm_point_with_area_entity_1.OsmPointWithArea)),
__param(5, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
__metadata("design:paramtypes", [typeof (_a = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _a : Object, typeof (_b = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _b : Object, typeof (_c = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _c : Object, typeof (_d = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _d : Object, typeof (_e = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _e : Object, Object])
__param(3, (0, typeorm_1.InjectRepository)(place_iraq_entity_1.PlaceIraq)),
__param(4, (0, typeorm_1.InjectRepository)(osm_area_entity_1.OsmArea)),
__param(5, (0, typeorm_1.InjectRepository)(osm_point_with_area_entity_1.OsmPointWithArea)),
__param(6, (0, common_1.Inject)(cache_manager_1.CACHE_MANAGER)),
__metadata("design:paramtypes", [typeof (_a = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _a : Object, typeof (_b = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _b : Object, typeof (_c = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _c : Object, typeof (_d = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _d : Object, typeof (_e = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _e : Object, typeof (_f = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _f : Object, Object])
], GeocodingService);
//# sourceMappingURL=geocoding.service.js.map
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -3,14 +3,16 @@ import { MapCandidate, CandidateStatus } from './entities/map-candidate.entity';
import { PlaceJordan } from './entities/place-jordan.entity';
import { PlaceSyria } from './entities/place-syria.entity';
import { PlaceEgypt } from './entities/place-egypt.entity';
import { PlaceIraq } from './entities/place-iraq.entity';
export declare class MapRefinementService {
private dataSource;
private candidateRepository;
private jordanRepository;
private syriaRepository;
private egyptRepository;
private iraqRepository;
private readonly logger;
constructor(dataSource: DataSource, candidateRepository: Repository<MapCandidate>, jordanRepository: Repository<PlaceJordan>, syriaRepository: Repository<PlaceSyria>, egyptRepository: Repository<PlaceEgypt>);
constructor(dataSource: DataSource, candidateRepository: Repository<MapCandidate>, jordanRepository: Repository<PlaceJordan>, syriaRepository: Repository<PlaceSyria>, egyptRepository: Repository<PlaceEgypt>, iraqRepository: Repository<PlaceIraq>);
suggestPlace(dto: any, submittedBy: string): Promise<MapCandidate>;
runExternalVerification(candidateId: number): Promise<void>;
getCandidates(status?: CandidateStatus): Promise<any[]>;
+10 -3
View File
@@ -12,7 +12,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var MapRefinementService_1;
var _a, _b, _c, _d, _e;
var _a, _b, _c, _d, _e, _f;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MapRefinementService = void 0;
const common_1 = require("@nestjs/common");
@@ -22,19 +22,22 @@ const map_candidate_entity_1 = require("./entities/map-candidate.entity");
const place_jordan_entity_1 = require("./entities/place-jordan.entity");
const place_syria_entity_1 = require("./entities/place-syria.entity");
const place_egypt_entity_1 = require("./entities/place-egypt.entity");
const place_iraq_entity_1 = require("./entities/place-iraq.entity");
let MapRefinementService = MapRefinementService_1 = class MapRefinementService {
dataSource;
candidateRepository;
jordanRepository;
syriaRepository;
egyptRepository;
iraqRepository;
logger = new common_1.Logger(MapRefinementService_1.name);
constructor(dataSource, candidateRepository, jordanRepository, syriaRepository, egyptRepository) {
constructor(dataSource, candidateRepository, jordanRepository, syriaRepository, egyptRepository, iraqRepository) {
this.dataSource = dataSource;
this.candidateRepository = candidateRepository;
this.jordanRepository = jordanRepository;
this.syriaRepository = syriaRepository;
this.egyptRepository = egyptRepository;
this.iraqRepository = iraqRepository;
}
async suggestPlace(dto, submittedBy) {
const lat = parseFloat(dto.lat || dto.latitude);
@@ -178,6 +181,9 @@ let MapRefinementService = MapRefinementService_1 = class MapRefinementService {
case map_candidate_entity_1.CountryCode.EGYPT:
repo = this.egyptRepository;
break;
case map_candidate_entity_1.CountryCode.IRAQ:
repo = this.iraqRepository;
break;
default: repo = this.jordanRepository;
}
const approvedPlace = repo.create(placeData);
@@ -203,6 +209,7 @@ exports.MapRefinementService = MapRefinementService = MapRefinementService_1 = _
__param(2, (0, typeorm_1.InjectRepository)(place_jordan_entity_1.PlaceJordan)),
__param(3, (0, typeorm_1.InjectRepository)(place_syria_entity_1.PlaceSyria)),
__param(4, (0, typeorm_1.InjectRepository)(place_egypt_entity_1.PlaceEgypt)),
__metadata("design:paramtypes", [typeof (_a = typeof typeorm_2.DataSource !== "undefined" && typeorm_2.DataSource) === "function" ? _a : Object, typeof (_b = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _b : Object, typeof (_c = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _c : Object, typeof (_d = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _d : Object, typeof (_e = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _e : Object])
__param(5, (0, typeorm_1.InjectRepository)(place_iraq_entity_1.PlaceIraq)),
__metadata("design:paramtypes", [typeof (_a = typeof typeorm_2.DataSource !== "undefined" && typeorm_2.DataSource) === "function" ? _a : Object, typeof (_b = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _b : Object, typeof (_c = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _c : Object, typeof (_d = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _d : Object, typeof (_e = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _e : Object, typeof (_f = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _f : Object])
], MapRefinementService);
//# sourceMappingURL=map-refinement.service.js.map
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -16,6 +16,7 @@ const road_stat_entity_1 = require("./road-stat.entity");
const candidate_road_entity_1 = require("./candidate-road.entity");
const redis_module_1 = require("../common/redis.module");
const geocoding_module_1 = require("../geocoding/geocoding.module");
const road_refinement_controller_1 = require("./road-refinement.controller");
let MapsModule = class MapsModule {
};
exports.MapsModule = MapsModule;
@@ -26,7 +27,7 @@ exports.MapsModule = MapsModule = __decorate([
redis_module_1.RedisModule,
geocoding_module_1.GeocodingModule,
],
controllers: [maps_controller_1.MapsController],
controllers: [maps_controller_1.MapsController, road_refinement_controller_1.RoadRefinementController],
providers: [maps_service_1.MapsService, traffic_grid_service_1.TrafficGridService],
exports: [maps_service_1.MapsService, traffic_grid_service_1.TrafficGridService],
})
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"maps.module.js","sourceRoot":"","sources":["../../src/maps/maps.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,6CAAgD;AAChD,iDAA6C;AAC7C,iEAA4D;AAC5D,uDAAmD;AACnD,yDAAqD;AACrD,mEAAwD;AACxD,yDAAqD;AACrD,oEAAgE;AAYzD,IAAM,UAAU,GAAhB,MAAM,UAAU;CAAG,CAAA;AAAb,gCAAU;qBAAV,UAAU;IAVtB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,uBAAa,CAAC,UAAU,CAAC,CAAC,kCAAe,EAAE,qCAAa,CAAC,CAAC;YAC1D,0BAAW;YACX,kCAAe;SAChB;QACD,WAAW,EAAE,CAAC,gCAAc,CAAC;QAC7B,SAAS,EAAE,CAAC,0BAAW,EAAE,yCAAkB,CAAC;QAC5C,OAAO,EAAE,CAAC,0BAAW,EAAE,yCAAkB,CAAC;KAC3C,CAAC;GACW,UAAU,CAAG"}
{"version":3,"file":"maps.module.js","sourceRoot":"","sources":["../../src/maps/maps.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,6CAAgD;AAChD,iDAA6C;AAC7C,iEAA4D;AAC5D,uDAAmD;AACnD,yDAAqD;AACrD,mEAAwD;AACxD,yDAAqD;AACrD,oEAAgE;AAEhE,6EAAwE;AAYjE,IAAM,UAAU,GAAhB,MAAM,UAAU;CAAG,CAAA;AAAb,gCAAU;qBAAV,UAAU;IAVtB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,uBAAa,CAAC,UAAU,CAAC,CAAC,kCAAe,EAAE,qCAAa,CAAC,CAAC;YAC1D,0BAAW;YACX,kCAAe;SAChB;QACD,WAAW,EAAE,CAAC,gCAAc,EAAE,qDAAwB,CAAC;QACvD,SAAS,EAAE,CAAC,0BAAW,EAAE,yCAAkB,CAAC;QAC5C,OAAO,EAAE,CAAC,0BAAW,EAAE,yCAAkB,CAAC;KAC3C,CAAC;GACW,UAAU,CAAG"}
+57
View File
@@ -0,0 +1,57 @@
import { Repository, DataSource } from 'typeorm';
import { CandidateRoad } from './candidate-road.entity';
import { RoadSegmentStat } from './road-stat.entity';
export declare class RoadRefinementController {
private readonly candidateRepo;
private readonly roadStatRepo;
private readonly dataSource;
private readonly logger;
constructor(candidateRepo: Repository<CandidateRoad>, roadStatRepo: Repository<RoadSegmentStat>, dataSource: DataSource);
getSummary(): Promise<{
roads: {
analyzed: number;
closed: number;
};
candidates: {
total: number;
pending: number;
};
}>;
getCandidates(status?: string): Promise<any>;
submitManualCandidate(body: {
geojson: any;
name?: string;
highway?: string;
}): Promise<{
success: boolean;
candidate: any;
}>;
approveCandidate(id: string): Promise<{
success: boolean;
id: string;
status: string;
}>;
rejectCandidate(id: string): Promise<{
success: boolean;
id: string;
status: string;
}>;
getClosures(): Promise<any>;
discoverOvertureGaps(): Promise<{
success: boolean;
candidatesFound: number;
message: string;
} | {
success: boolean;
candidatesFound: any;
message?: undefined;
}>;
discoverClosures(): Promise<{
success: boolean;
roadsClosed: number;
}>;
snapExistingApprovedRoads(): Promise<{
success: boolean;
message: string;
}>;
}
+613
View File
@@ -0,0 +1,613 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var RoadRefinementController_1;
var _a, _b, _c;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RoadRefinementController = void 0;
const common_1 = require("@nestjs/common");
const swagger_1 = require("@nestjs/swagger");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const candidate_road_entity_1 = require("./candidate-road.entity");
const road_stat_entity_1 = require("./road-stat.entity");
const api_key_guard_1 = require("../common/guards/api-key.guard");
let RoadRefinementController = RoadRefinementController_1 = class RoadRefinementController {
candidateRepo;
roadStatRepo;
dataSource;
logger = new common_1.Logger(RoadRefinementController_1.name);
constructor(candidateRepo, roadStatRepo, dataSource) {
this.candidateRepo = candidateRepo;
this.roadStatRepo = roadStatRepo;
this.dataSource = dataSource;
}
async getSummary() {
try {
const [cCount] = await this.dataSource.query("SELECT COUNT(*) as total, COUNT(*) FILTER (WHERE status='pending') as pending FROM candidate_roads");
const [clCount] = await this.dataSource.query('SELECT COUNT(*) as count FROM road_segment_stats WHERE "isClosed" = true');
const [rCount] = await this.dataSource.query('SELECT COUNT(*) as count FROM road_segment_stats');
return {
roads: {
analyzed: parseInt(rCount?.count || '0', 10),
closed: parseInt(clCount?.count || '0', 10),
},
candidates: {
total: parseInt(cCount?.total || '0', 10),
pending: parseInt(cCount?.pending || '0', 10),
},
};
}
catch (e) {
this.logger.error(`Error in summary: ${e.message}`);
return {
roads: { analyzed: 0, closed: 0 },
candidates: { total: 0, pending: 0 },
};
}
}
async getCandidates(status) {
const s = status || 'pending';
try {
const rows = await this.dataSource.query(`SELECT id, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
confidence, status, source, name, highway, oneway, "discoveredAt", "reviewedAt",
ST_AsGeoJSON(geometry) as geojson
FROM candidate_roads
WHERE status = $1
ORDER BY confidence DESC, "discoveredAt" DESC
LIMIT 100`, [s]);
return rows.map((r) => ({
...r,
geometry: r.geojson ? JSON.parse(r.geojson) : null,
}));
}
catch (e) {
this.logger.error(`Error fetching candidates: ${e.message}`);
return [];
}
}
async submitManualCandidate(body) {
if (!body.geojson || !body.geojson.coordinates || body.geojson.coordinates.length < 2) {
throw new common_1.HttpException('Invalid GeoJSON LineString coordinates', common_1.HttpStatus.BAD_REQUEST);
}
try {
const geojsonStr = JSON.stringify(body.geojson);
const name = body.name || 'Unnamed Road';
const highway = body.highway || 'residential';
let result;
try {
result = await this.dataSource.query(`WITH raw_input AS (
SELECT ST_SetSRID(ST_GeomFromGeoJSON($1), 4326) AS geom
),
start_snap AS (
SELECT COALESCE(
(
SELECT ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)
FROM raw_input r
CROSS JOIN LATERAL (SELECT ST_Transform(ST_StartPoint(r.geom), 3857) AS pt) p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
JOIN planet_osm_ways w ON w.id = l.osm_id
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
JOIN planet_osm_nodes n ON n.id = wn.node_id
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_StartPoint(r.geom)::geography) < 30
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_StartPoint(r.geom)::geography) ASC
LIMIT 1
),
(
SELECT ST_Transform(ST_ClosestPoint(l.way, ST_Transform(ST_StartPoint(r.geom), 3857)), 4326)
FROM raw_input r
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(ST_Transform(ST_StartPoint(r.geom), 3857), 60)
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, ST_StartPoint(r.geom)::geography) < 30
ORDER BY ST_Distance(l.way, ST_Transform(ST_StartPoint(r.geom), 3857)) ASC
LIMIT 1
),
(SELECT ST_StartPoint(geom) FROM raw_input)
) AS pt
),
end_snap AS (
SELECT COALESCE(
(
SELECT ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)
FROM raw_input r
CROSS JOIN LATERAL (SELECT ST_Transform(ST_EndPoint(r.geom), 3857) AS pt) p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
JOIN planet_osm_ways w ON w.id = l.osm_id
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
JOIN planet_osm_nodes n ON n.id = wn.node_id
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_EndPoint(r.geom)::geography) < 30
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, ST_EndPoint(r.geom)::geography) ASC
LIMIT 1
),
(
SELECT ST_Transform(ST_ClosestPoint(l.way, ST_Transform(ST_EndPoint(r.geom), 3857)), 4326)
FROM raw_input r
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(ST_Transform(ST_EndPoint(r.geom), 3857), 60)
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, ST_EndPoint(r.geom)::geography) < 30
ORDER BY ST_Distance(l.way, ST_Transform(ST_EndPoint(r.geom), 3857)) ASC
LIMIT 1
),
(SELECT ST_EndPoint(geom) FROM raw_input)
) AS pt
),
snapped_input AS (
SELECT ST_SetPoint(
ST_SetPoint(r.geom, 0, s.pt),
ST_NPoints(r.geom) - 1,
e.pt
) AS geom
FROM raw_input r, start_snap s, end_snap e
)
INSERT INTO candidate_roads (
geometry, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
confidence, status, source, name, highway, oneway, "discoveredAt"
)
SELECT
geom,
1, 10, 30.0,
ST_Length(ST_Transform(geom, 3857)),
0.90, 'pending', 'manual', $2, $3, 0, NOW()
FROM snapped_input
RETURNING id, name, highway, confidence, status`, [geojsonStr, name, highway]);
}
catch (err) {
this.logger.warn(`Complex snapping query failed, falling back to direct insertion: ${err.message}`);
result = await this.dataSource.query(`INSERT INTO candidate_roads (
geometry, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters",
confidence, status, source, name, highway, oneway, "discoveredAt"
)
VALUES (
ST_SetSRID(ST_GeomFromGeoJSON($1), 4326),
1, 10, 30.0,
ST_Length(ST_Transform(ST_SetSRID(ST_GeomFromGeoJSON($1), 4326), 3857)),
0.90, 'pending', 'manual', $2, $3, 0, NOW()
)
RETURNING id, name, highway, confidence, status`, [geojsonStr, name, highway]);
}
this.logger.log(`✍️ Manual candidate road registered: ${result[0]?.id} (${name})`);
return { success: true, candidate: result[0] };
}
catch (e) {
this.logger.error(`Failed to insert manual candidate road: ${e.message}`);
throw new common_1.HttpException(`Failed to create candidate road: ${e.message}`, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async approveCandidate(id) {
try {
await this.dataSource.query(`
CREATE TABLE IF NOT EXISTS approved_roads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
candidate_id UUID UNIQUE,
geometry GEOMETRY(LineString, 4326),
name VARCHAR(255),
highway VARCHAR(32),
confidence FLOAT,
"uniqueDriverCount" INT DEFAULT 1,
oneway SMALLINT DEFAULT 0,
start_node BIGINT,
end_node BIGINT,
approved_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS approved_roads_geom_idx ON approved_roads USING GIST(geometry);
CREATE UNIQUE INDEX IF NOT EXISTS approved_roads_cand_uidx ON approved_roads(candidate_id) WHERE candidate_id IS NOT NULL;
`);
await this.candidateRepo.update(id, {
status: 'approved',
reviewedAt: new Date(),
});
await this.dataSource.query(`
DO $$
DECLARE
cand RECORD;
start_pt GEOMETRY;
end_pt GEOMETRY;
snapped_geom GEOMETRY;
s_node BIGINT := NULL;
e_node BIGINT := NULL;
node_rec RECORD;
line_rec RECORD;
BEGIN
SELECT id, geometry::geometry as geom, name, COALESCE(highway, 'residential') as highway, confidence, "uniqueDriverCount", oneway
INTO cand
FROM candidate_roads WHERE id = $1;
IF FOUND THEN
snapped_geom := cand.geom;
start_pt := ST_StartPoint(snapped_geom);
end_pt := ST_EndPoint(snapped_geom);
-- 1. Start node & geometry snapping
BEGIN
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
INTO node_rec
FROM p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
JOIN planet_osm_ways w ON w.id = l.osm_id
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
JOIN planet_osm_nodes n ON n.id = wn.node_id
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
LIMIT 1;
IF FOUND THEN
s_node := node_rec.id;
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
ELSE
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
INTO line_rec
FROM p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, start_pt::geography) < 30
ORDER BY ST_Distance(l.way, p.pt) ASC
LIMIT 1;
IF FOUND THEN
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
END IF;
END IF;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
-- 2. End node & geometry snapping
BEGIN
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
INTO node_rec
FROM p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
JOIN planet_osm_ways w ON w.id = l.osm_id
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
JOIN planet_osm_nodes n ON n.id = wn.node_id
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
LIMIT 1;
IF FOUND THEN
e_node := node_rec.id;
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
ELSE
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
INTO line_rec
FROM p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, end_pt::geography) < 30
ORDER BY ST_Distance(l.way, p.pt) ASC
LIMIT 1;
IF FOUND THEN
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
END IF;
END IF;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
-- Update candidate_roads
UPDATE candidate_roads SET geometry = snapped_geom WHERE id = cand.id;
-- Insert or update approved_roads
INSERT INTO approved_roads (
candidate_id, geometry, name, highway, confidence, "uniqueDriverCount", oneway, start_node, end_node
)
VALUES (
cand.id, snapped_geom, cand.name, cand.highway, cand.confidence, cand."uniqueDriverCount", cand.oneway, s_node, e_node
)
ON CONFLICT (candidate_id) DO UPDATE SET
geometry = EXCLUDED.geometry,
start_node = EXCLUDED.start_node,
end_node = EXCLUDED.end_node;
END IF;
END $$;
`, [id]);
try {
await this.dataSource.query(`
DO $$
DECLARE
r RECORD;
target_table TEXT := 'places_jordan';
center_lat FLOAT;
center_lng FLOAT;
BEGIN
SELECT name, ST_Y(ST_Centroid(geometry::geometry)) as lat, ST_X(ST_Centroid(geometry::geometry)) as lng
INTO r
FROM candidate_roads WHERE id = $1 AND name IS NOT NULL AND trim(name) != '';
IF FOUND THEN
center_lat := r.lat;
center_lng := r.lng;
IF center_lat >= 29 AND center_lat <= 37.5 AND center_lng >= 38.7 AND center_lng <= 48.8 THEN
target_table := 'places_iraq';
ELSIF center_lat >= 29 AND center_lat <= 37.5 AND center_lng >= 34.5 AND center_lng <= 42.5 THEN
IF center_lat > 32.5 AND center_lng > 35.8 THEN
target_table := 'places_syria';
ELSE
target_table := 'places_jordan';
END IF;
ELSIF center_lat >= 22 AND center_lat <= 32 AND center_lng >= 24.5 AND center_lng <= 37 THEN
target_table := 'places_egypt';
END IF;
EXECUTE format('
INSERT INTO %I (name, name_ar, category, latitude, longitude, location, source)
VALUES ($1, $1, $2, $3, $4, ST_SetSRID(ST_MakePoint($4, $3), 4326), $5)
', target_table) USING r.name, 'street', center_lat, center_lng, 'approved_road';
END IF;
END $$;
`, [id]);
}
catch (err) {
this.logger.warn(`Could not index approved road to places: ${err.message}`);
}
this.logger.log(`✅ Road candidate ${id} approved & published.`);
return { success: true, id, status: 'approved' };
}
catch (e) {
this.logger.error(`Failed to approve candidate road: ${e.message}`);
throw new common_1.HttpException(e.message, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async rejectCandidate(id) {
try {
await this.candidateRepo.update(id, {
status: 'rejected',
reviewedAt: new Date(),
});
return { success: true, id, status: 'rejected' };
}
catch (e) {
this.logger.error(`Failed to reject candidate road: ${e.message}`);
throw new common_1.HttpException(e.message, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
}
}
async getClosures() {
return this.roadStatRepo.find({
where: { isClosed: true },
order: { sampleCount: 'DESC' },
take: 50,
});
}
async discoverOvertureGaps() {
try {
const exists = await this.dataSource.query(`
SELECT to_regclass('public.overture_transportation') as tbl;
`);
if (!exists[0]?.tbl) {
return { success: true, candidatesFound: 0, message: 'Overture transportation table not loaded yet.' };
}
const inserted = await this.dataSource.query(`
INSERT INTO candidate_roads (geometry, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters", confidence, status, source, name, highway, oneway)
SELECT
o.geometry,
5, 50, 40.0,
ST_Length(ST_Transform(o.geometry::geometry, 3857)),
0.85,
'pending',
'overture',
o.name,
COALESCE(o.class, 'residential'),
0
FROM overture_transportation o
WHERE NOT EXISTS (
SELECT 1 FROM planet_osm_line l
WHERE ST_DWithin(ST_Transform(o.geometry::geometry, 3857), l.way, 25)
)
AND ST_Length(ST_Transform(o.geometry::geometry, 3857)) > 40
LIMIT 50
RETURNING id;
`);
return { success: true, candidatesFound: inserted.length };
}
catch (e) {
this.logger.warn(`Overture gap check skipped: ${e.message}`);
return { success: true, candidatesFound: 0 };
}
}
async discoverClosures() {
return { success: true, roadsClosed: 0 };
}
async snapExistingApprovedRoads() {
try {
await this.dataSource.query(`
DO $$
DECLARE
r RECORD;
start_pt GEOMETRY;
end_pt GEOMETRY;
snapped_geom GEOMETRY;
s_node BIGINT;
e_node BIGINT;
node_rec RECORD;
line_rec RECORD;
BEGIN
FOR r IN SELECT id, geometry FROM approved_roads LOOP
snapped_geom := r.geometry;
s_node := NULL;
e_node := NULL;
start_pt := ST_StartPoint(snapped_geom);
end_pt := ST_EndPoint(snapped_geom);
-- Snap start
BEGIN
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
INTO node_rec
FROM p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
JOIN planet_osm_ways w ON w.id = l.osm_id
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
JOIN planet_osm_nodes n ON n.id = wn.node_id
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) < 30
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, start_pt::geography) ASC
LIMIT 1;
IF FOUND THEN
s_node := node_rec.id;
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
ELSE
WITH p AS (SELECT ST_Transform(start_pt, 3857) AS pt)
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
INTO line_rec
FROM p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, start_pt::geography) < 30
ORDER BY ST_Distance(l.way, p.pt) ASC
LIMIT 1;
IF FOUND THEN
snapped_geom := ST_SetPoint(snapped_geom, 0, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
END IF;
END IF;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
-- Snap end
BEGIN
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
SELECT n.id, n.lon/1e7 as lon, n.lat/1e7 as lat
INTO node_rec
FROM p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
JOIN planet_osm_ways w ON w.id = l.osm_id
CROSS JOIN LATERAL unnest(w.nodes) AS wn(node_id)
JOIN planet_osm_nodes n ON n.id = wn.node_id
WHERE ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) < 30
ORDER BY ST_Distance(ST_SetSRID(ST_MakePoint(n.lon/1e7, n.lat/1e7), 4326)::geography, end_pt::geography) ASC
LIMIT 1;
IF FOUND THEN
e_node := node_rec.id;
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(node_rec.lon, node_rec.lat), 4326));
ELSE
WITH p AS (SELECT ST_Transform(end_pt, 3857) AS pt)
SELECT ST_X(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lon,
ST_Y(ST_Transform(ST_ClosestPoint(l.way, p.pt), 4326)) as lat
INTO line_rec
FROM p
JOIN planet_osm_line l ON l.highway IS NOT NULL AND l.way && ST_Expand(p.pt, 60)
WHERE ST_Distance(ST_Transform(l.way, 4326)::geography, end_pt::geography) < 30
ORDER BY ST_Distance(l.way, p.pt) ASC
LIMIT 1;
IF FOUND THEN
snapped_geom := ST_SetPoint(snapped_geom, ST_NPoints(snapped_geom) - 1, ST_SetSRID(ST_MakePoint(line_rec.lon, line_rec.lat), 4326));
END IF;
END IF;
EXCEPTION WHEN OTHERS THEN
NULL;
END;
UPDATE approved_roads
SET geometry = snapped_geom,
start_node = COALESCE(s_node, start_node),
end_node = COALESCE(e_node, end_node)
WHERE id = r.id;
END LOOP;
END $$;
`);
return { success: true, message: 'All approved roads snapped & nodes resolved' };
}
catch (e) {
this.logger.error(`Failed to snap existing roads: ${e.message}`);
throw new common_1.HttpException(e.message, common_1.HttpStatus.INTERNAL_SERVER_ERROR);
}
}
};
exports.RoadRefinementController = RoadRefinementController;
__decorate([
(0, common_1.Get)('summary'),
(0, swagger_1.ApiOperation)({ summary: 'Get road intelligence summary 📊' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], RoadRefinementController.prototype, "getSummary", null);
__decorate([
(0, common_1.Get)('candidates'),
(0, swagger_1.ApiOperation)({ summary: 'Get candidate roads list 🛣️' }),
(0, swagger_1.ApiQuery)({ name: 'status', required: false }),
__param(0, (0, common_1.Query)('status')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], RoadRefinementController.prototype, "getCandidates", null);
__decorate([
(0, common_1.Post)('candidates/manual'),
(0, swagger_1.ApiOperation)({ summary: 'Submit manual candidate road drawn on map ✍️' }),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], RoadRefinementController.prototype, "submitManualCandidate", null);
__decorate([
(0, common_1.Patch)('candidates/:id/approve'),
(0, swagger_1.ApiOperation)({ summary: 'Approve candidate road and move to approved_roads with auto-snapped topology ✅' }),
(0, swagger_1.ApiParam)({ name: 'id', description: 'Candidate UUID' }),
__param(0, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], RoadRefinementController.prototype, "approveCandidate", null);
__decorate([
(0, common_1.Patch)('candidates/:id/reject'),
(0, swagger_1.ApiOperation)({ summary: 'Reject candidate road ❌' }),
(0, swagger_1.ApiParam)({ name: 'id', description: 'Candidate UUID' }),
__param(0, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], RoadRefinementController.prototype, "rejectCandidate", null);
__decorate([
(0, common_1.Get)('closures'),
(0, swagger_1.ApiOperation)({ summary: 'Get active road closures 🚧' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], RoadRefinementController.prototype, "getClosures", null);
__decorate([
(0, common_1.Post)('discover-overture-gaps'),
(0, swagger_1.ApiOperation)({ summary: 'Discover missing roads against Overture Maps 🗺️' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], RoadRefinementController.prototype, "discoverOvertureGaps", null);
__decorate([
(0, common_1.Post)('discover-closures'),
(0, swagger_1.ApiOperation)({ summary: 'Discover closures 🚧' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], RoadRefinementController.prototype, "discoverClosures", null);
__decorate([
(0, common_1.Post)('snap-existing-roads'),
(0, swagger_1.ApiOperation)({ summary: 'Auto-snap and resolve topology nodes for all existing approved roads' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], RoadRefinementController.prototype, "snapExistingApprovedRoads", null);
exports.RoadRefinementController = RoadRefinementController = RoadRefinementController_1 = __decorate([
(0, swagger_1.ApiTags)('map-refinement-roads'),
(0, common_1.Controller)('map-refinement/roads'),
(0, common_1.UseGuards)(api_key_guard_1.ApiKeyGuard),
__param(0, (0, typeorm_1.InjectRepository)(candidate_road_entity_1.CandidateRoad)),
__param(1, (0, typeorm_1.InjectRepository)(road_stat_entity_1.RoadSegmentStat)),
__metadata("design:paramtypes", [typeof (_a = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _a : Object, typeof (_b = typeof typeorm_2.Repository !== "undefined" && typeorm_2.Repository) === "function" ? _b : Object, typeof (_c = typeof typeorm_2.DataSource !== "undefined" && typeorm_2.DataSource) === "function" ? _c : Object])
], RoadRefinementController);
//# sourceMappingURL=road-refinement.controller.js.map
File diff suppressed because one or more lines are too long
+29
View File
@@ -0,0 +1,29 @@
export declare class PointDto {
lat: number;
lng: number;
height?: number;
}
export declare class LineOfSightQueryDto {
observerLat: number;
observerLng: number;
targetLat: number;
targetLng: number;
observerHeight?: number;
targetHeight?: number;
stepMeters?: number;
samples?: number;
compact?: boolean;
}
export declare class LineOfSightBodyDto {
observer?: PointDto;
target?: PointDto;
observerLat?: number;
observerLng?: number;
targetLat?: number;
targetLng?: number;
observerHeight?: number;
targetHeight?: number;
stepMeters?: number;
samples?: number;
compact?: boolean;
}
+209
View File
@@ -0,0 +1,209 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.LineOfSightBodyDto = exports.LineOfSightQueryDto = exports.PointDto = void 0;
const swagger_1 = require("@nestjs/swagger");
const class_transformer_1 = require("class-transformer");
const class_validator_1 = require("class-validator");
class PointDto {
lat;
lng;
height;
}
exports.PointDto = PointDto;
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Latitude in decimal degrees', example: 32.311852 }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], PointDto.prototype, "lat", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Longitude in decimal degrees', example: 36.839237 }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], PointDto.prototype, "lng", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Height offset above ground in meters (e.g. eye level / mast)', example: 2, default: 2 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], PointDto.prototype, "height", void 0);
class LineOfSightQueryDto {
observerLat;
observerLng;
targetLat;
targetLng;
observerHeight;
targetHeight;
stepMeters;
samples;
compact;
}
exports.LineOfSightQueryDto = LineOfSightQueryDto;
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Observer (Point A) Latitude', example: 32.311852 }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightQueryDto.prototype, "observerLat", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Observer (Point A) Longitude', example: 36.839237 }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightQueryDto.prototype, "observerLng", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Target (Point B) Latitude', example: 33.371104 }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightQueryDto.prototype, "targetLat", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Target (Point B) Longitude', example: 38.793024 }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightQueryDto.prototype, "targetLng", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Observer eye level / sensor height above ground (meters)', default: 2 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(0),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightQueryDto.prototype, "observerHeight", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Target height above ground (meters)', default: 2 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(0),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightQueryDto.prototype, "targetHeight", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Sampling distance step in meters (e.g. 5, 10, 25, 50). If omitted, automatically optimized.' }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(1),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightQueryDto.prototype, "stepMeters", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Explicit number of sample points (5 - 300). If omitted, dynamically calculated.' }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(5),
(0, class_validator_1.Max)(300),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightQueryDto.prototype, "samples", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'If true, returns only executive summary and obstacle info without profile array', default: false }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsBoolean)(),
(0, class_transformer_1.Type)(() => Boolean),
__metadata("design:type", Boolean)
], LineOfSightQueryDto.prototype, "compact", void 0);
class LineOfSightBodyDto {
observer;
target;
observerLat;
observerLng;
targetLat;
targetLng;
observerHeight;
targetHeight;
stepMeters;
samples;
compact;
}
exports.LineOfSightBodyDto = LineOfSightBodyDto;
__decorate([
(0, swagger_1.ApiPropertyOptional)({ type: PointDto }),
(0, class_validator_1.IsOptional)(),
__metadata("design:type", PointDto)
], LineOfSightBodyDto.prototype, "observer", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ type: PointDto }),
(0, class_validator_1.IsOptional)(),
__metadata("design:type", PointDto)
], LineOfSightBodyDto.prototype, "target", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Observer Latitude (flat format)', example: 32.311852 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightBodyDto.prototype, "observerLat", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Observer Longitude (flat format)', example: 36.839237 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightBodyDto.prototype, "observerLng", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Target Latitude (flat format)', example: 33.371104 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightBodyDto.prototype, "targetLat", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Target Longitude (flat format)', example: 38.793024 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightBodyDto.prototype, "targetLng", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Observer height offset (meters)', default: 2 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(0),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightBodyDto.prototype, "observerHeight", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Target height offset (meters)', default: 2 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(0),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightBodyDto.prototype, "targetHeight", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Sampling distance step in meters (e.g. 5, 10, 25, 50)' }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(1),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightBodyDto.prototype, "stepMeters", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Number of sample points (5 - 300)' }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_validator_1.Min)(5),
(0, class_validator_1.Max)(300),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], LineOfSightBodyDto.prototype, "samples", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'If true, returns only executive summary and obstacle info without profile array', default: false }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsBoolean)(),
(0, class_transformer_1.Type)(() => Boolean),
__metadata("design:type", Boolean)
], LineOfSightBodyDto.prototype, "compact", void 0);
//# sourceMappingURL=line-of-sight.dto.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"line-of-sight.dto.js","sourceRoot":"","sources":["../../../src/tactical/dto/line-of-sight.dto.ts"],"names":[],"mappings":"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"}
+26
View File
@@ -0,0 +1,26 @@
export declare class ArtilleryMissionRequestDto {
gunLat: number;
gunLng: number;
targetLat: number;
targetLng: number;
gunElevationOffset?: number;
targetElevationOffset?: number;
chargeType?: string;
muzzleVelocity?: number;
caliber?: string;
}
export declare class TacticalSymbolDto {
id: string;
type: string;
name: string;
name_ar: string;
lat: number;
lng: number;
elevation?: number;
notes?: string;
timeAdded: string;
}
export declare class SaveScenarioDto {
name: string;
symbols: TacticalSymbolDto[];
}
+162
View File
@@ -0,0 +1,162 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SaveScenarioDto = exports.TacticalSymbolDto = exports.ArtilleryMissionRequestDto = void 0;
const swagger_1 = require("@nestjs/swagger");
const class_transformer_1 = require("class-transformer");
const class_validator_1 = require("class-validator");
class ArtilleryMissionRequestDto {
gunLat;
gunLng;
targetLat;
targetLng;
gunElevationOffset;
targetElevationOffset;
chargeType;
muzzleVelocity;
caliber;
}
exports.ArtilleryMissionRequestDto = ArtilleryMissionRequestDto;
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Gun / Battery Latitude', example: 31.9539 }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], ArtilleryMissionRequestDto.prototype, "gunLat", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Gun / Battery Longitude', example: 35.9106 }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], ArtilleryMissionRequestDto.prototype, "gunLng", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Target Latitude', example: 32.01 }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], ArtilleryMissionRequestDto.prototype, "targetLat", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Target Longitude', example: 35.87 }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], ArtilleryMissionRequestDto.prototype, "targetLng", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Gun elevation offset above ground in meters', default: 2 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], ArtilleryMissionRequestDto.prototype, "gunElevationOffset", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Target elevation offset above ground in meters', default: 0 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], ArtilleryMissionRequestDto.prototype, "targetElevationOffset", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Powder charge type (Low, Med, High)' }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], ArtilleryMissionRequestDto.prototype, "chargeType", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Muzzle velocity in m/s (default: 827 m/s for 155mm Howitzer)', default: 827 }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], ArtilleryMissionRequestDto.prototype, "muzzleVelocity", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Weapon Caliber description', default: '155mm Howitzer' }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], ArtilleryMissionRequestDto.prototype, "caliber", void 0);
class TacticalSymbolDto {
id;
type;
name;
name_ar;
lat;
lng;
elevation;
notes;
timeAdded;
}
exports.TacticalSymbolDto = TacticalSymbolDto;
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Symbol Unique ID' }),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], TacticalSymbolDto.prototype, "id", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Symbol military type' }),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], TacticalSymbolDto.prototype, "type", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Name in English' }),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], TacticalSymbolDto.prototype, "name", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Name in Arabic' }),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], TacticalSymbolDto.prototype, "name_ar", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Latitude' }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], TacticalSymbolDto.prototype, "lat", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Longitude' }),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], TacticalSymbolDto.prototype, "lng", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Elevation' }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsNumber)(),
(0, class_transformer_1.Type)(() => Number),
__metadata("design:type", Number)
], TacticalSymbolDto.prototype, "elevation", void 0);
__decorate([
(0, swagger_1.ApiPropertyOptional)({ description: 'Operational Notes' }),
(0, class_validator_1.IsOptional)(),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], TacticalSymbolDto.prototype, "notes", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Timestamp added' }),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], TacticalSymbolDto.prototype, "timeAdded", void 0);
class SaveScenarioDto {
name;
symbols;
}
exports.SaveScenarioDto = SaveScenarioDto;
__decorate([
(0, swagger_1.ApiProperty)({ description: 'Scenario Name', example: 'default' }),
(0, class_validator_1.IsString)(),
__metadata("design:type", String)
], SaveScenarioDto.prototype, "name", void 0);
__decorate([
(0, swagger_1.ApiProperty)({ description: 'List of tactical symbols', type: [TacticalSymbolDto] }),
(0, class_validator_1.IsArray)(),
__metadata("design:type", Array)
], SaveScenarioDto.prototype, "symbols", void 0);
//# sourceMappingURL=tactical.dto.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"tactical.dto.js","sourceRoot":"","sources":["../../../src/tactical/dto/tactical.dto.ts"],"names":[],"mappings":"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"}
+49
View File
@@ -0,0 +1,49 @@
import { LineOfSightBodyDto, LineOfSightQueryDto } from './dto/line-of-sight.dto';
import { ArtilleryMissionRequestDto, SaveScenarioDto } from './dto/tactical.dto';
import { TacticalService } from './tactical.service';
export declare class TacticalController {
private readonly tacticalService;
constructor(tacticalService: TacticalService);
getLineOfSight(query: LineOfSightQueryDto): Promise<any>;
getLosAlias(query: LineOfSightQueryDto): Promise<any>;
postLineOfSight(body: LineOfSightBodyDto): Promise<any>;
postLosAlias(body: LineOfSightBodyDto): Promise<any>;
calculateArtilleryFireMission(dto: ArtilleryMissionRequestDto): Promise<{
fireMissionId: string;
caliber: string;
muzzleVelocityMs: number;
distanceMeters: number;
distanceKm: number;
azimuthDegrees: number;
azimuthMils: number;
gunElevationMeters: number;
targetElevationMeters: number;
apexAltitudeMeters: number;
timeOfFlightSeconds: number;
lowAngle: {
degrees: number;
mils: number;
};
highAngle: {
degrees: number;
mils: number;
};
hasCrestClearance: boolean;
criticalObstacle: any;
trajectoryPoints: any[];
}>;
assessHLZ(lat: string, lng: string, radius?: string): Promise<{
centerLat: number;
centerLng: number;
searchRadiusMeters: number;
totalAssessed: number;
suitableCount: number;
zones: any[];
}>;
saveScenario(body: SaveScenarioDto): Promise<{
success: boolean;
count: number;
name: string;
}>;
getScenario(name?: string): Promise<import("./tactical.service").TacticalSymbol[]>;
}
+164
View File
@@ -0,0 +1,164 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TacticalController = void 0;
const common_1 = require("@nestjs/common");
const swagger_1 = require("@nestjs/swagger");
const api_key_guard_1 = require("../common/guards/api-key.guard");
const rate_limiter_guard_1 = require("../common/guards/rate-limiter.guard");
const line_of_sight_dto_1 = require("./dto/line-of-sight.dto");
const tactical_dto_1 = require("./dto/tactical.dto");
const tactical_service_1 = require("./tactical.service");
let TacticalController = class TacticalController {
tacticalService;
constructor(tacticalService) {
this.tacticalService = tacticalService;
}
async getLineOfSight(query) {
const { observerLat, observerLng, targetLat, targetLng, observerHeight = 2, targetHeight = 2, samples, stepMeters, compact = false, } = query;
if (observerLat == null || observerLng == null || targetLat == null || targetLng == null) {
throw new common_1.HttpException('Missing required parameters: observerLat, observerLng, targetLat, targetLng', common_1.HttpStatus.BAD_REQUEST);
}
return this.tacticalService.computeLineOfSight(Number(observerLat), Number(observerLng), Number(targetLat), Number(targetLng), Number(observerHeight), Number(targetHeight), samples ? Number(samples) : undefined, stepMeters ? Number(stepMeters) : undefined, Boolean(compact));
}
async getLosAlias(query) {
return this.getLineOfSight(query);
}
async postLineOfSight(body) {
const obsLat = body.observer?.lat ?? body.observerLat;
const obsLng = body.observer?.lng ?? body.observerLng;
const obsHeight = body.observer?.height ?? body.observerHeight ?? 2;
const tgtLat = body.target?.lat ?? body.targetLat;
const tgtLng = body.target?.lng ?? body.targetLng;
const tgtHeight = body.target?.height ?? body.targetHeight ?? 2;
const samples = body.samples;
const stepMeters = body.stepMeters;
const compact = body.compact ?? false;
if (obsLat == null || obsLng == null || tgtLat == null || tgtLng == null) {
throw new common_1.HttpException('Missing required parameters: observer coordinates (lat, lng) and target coordinates (lat, lng)', common_1.HttpStatus.BAD_REQUEST);
}
return this.tacticalService.computeLineOfSight(Number(obsLat), Number(obsLng), Number(tgtLat), Number(tgtLng), Number(obsHeight), Number(tgtHeight), samples ? Number(samples) : undefined, stepMeters ? Number(stepMeters) : undefined, Boolean(compact));
}
async postLosAlias(body) {
return this.postLineOfSight(body);
}
async calculateArtilleryFireMission(dto) {
return this.tacticalService.calculateArtilleryFireMission(dto);
}
async assessHLZ(lat, lng, radius) {
const latNum = parseFloat(lat || '31.95');
const lngNum = parseFloat(lng || '35.93');
const radiusNum = parseFloat(radius || '3000');
return this.tacticalService.assessHelicopterLandingZones(latNum, lngNum, radiusNum);
}
async saveScenario(body) {
return this.tacticalService.saveScenario(body.name || 'default', body.symbols || []);
}
async getScenario(name) {
return this.tacticalService.getScenario(name || 'default');
}
};
exports.TacticalController = TacticalController;
__decorate([
(0, common_1.Get)('line-of-sight'),
(0, swagger_1.ApiOperation)({
summary: 'Calculate Tactical Line of Sight & Intervisibility (تبادل الرؤية العسكري)',
description: 'Calculates line of sight between Observer (Point A) and Target (Point B) accounting for elevation profile, Earth curvature, atmospheric refraction, dead ground, and obstacle penetration.',
}),
__param(0, (0, common_1.Query)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [line_of_sight_dto_1.LineOfSightQueryDto]),
__metadata("design:returntype", Promise)
], TacticalController.prototype, "getLineOfSight", null);
__decorate([
(0, common_1.Get)('los'),
(0, swagger_1.ApiOperation)({ summary: 'Alias for line-of-sight GET' }),
__param(0, (0, common_1.Query)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [line_of_sight_dto_1.LineOfSightQueryDto]),
__metadata("design:returntype", Promise)
], TacticalController.prototype, "getLosAlias", null);
__decorate([
(0, common_1.Post)('line-of-sight'),
(0, swagger_1.ApiOperation)({
summary: 'Calculate Tactical Line of Sight via POST JSON body',
description: 'Accepts structured observer and target objects or flat coordinate keys.',
}),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [line_of_sight_dto_1.LineOfSightBodyDto]),
__metadata("design:returntype", Promise)
], TacticalController.prototype, "postLineOfSight", null);
__decorate([
(0, common_1.Post)('los'),
(0, swagger_1.ApiOperation)({ summary: 'Alias for line-of-sight POST' }),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [line_of_sight_dto_1.LineOfSightBodyDto]),
__metadata("design:returntype", Promise)
], TacticalController.prototype, "postLosAlias", null);
__decorate([
(0, common_1.Post)('artillery-fire-mission'),
(0, swagger_1.ApiOperation)({
summary: 'Calculate Artillery Fire Mission & Ballistic Trajectory / حساب رماية المدفعية ومسار القذيفة',
}),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [tactical_dto_1.ArtilleryMissionRequestDto]),
__metadata("design:returntype", Promise)
], TacticalController.prototype, "calculateArtilleryFireMission", null);
__decorate([
(0, common_1.Get)('hlz-assessment'),
(0, swagger_1.ApiOperation)({
summary: 'Assess Helicopter Landing Zones (HLZ) & MEDEVAC / كشف صلاحية مهابط المروحيات والإخلاء الطبي',
}),
__param(0, (0, common_1.Query)('lat')),
__param(1, (0, common_1.Query)('lng')),
__param(2, (0, common_1.Query)('radius')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String, String]),
__metadata("design:returntype", Promise)
], TacticalController.prototype, "assessHLZ", null);
__decorate([
(0, common_1.Post)('scenarios'),
(0, swagger_1.ApiOperation)({
summary: 'Save tactical symbols scenario / حفظ سيناريو الرموز التكتيكية',
}),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [tactical_dto_1.SaveScenarioDto]),
__metadata("design:returntype", Promise)
], TacticalController.prototype, "saveScenario", null);
__decorate([
(0, common_1.Get)('scenarios'),
(0, swagger_1.ApiOperation)({
summary: 'Get saved tactical symbols scenario / استرجاع سيناريو الرموز التكتيكية',
}),
__param(0, (0, common_1.Query)('name')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], TacticalController.prototype, "getScenario", null);
exports.TacticalController = TacticalController = __decorate([
(0, swagger_1.ApiTags)('tactical'),
(0, swagger_1.ApiHeader)({
name: 'x-api-key',
description: 'Multi-tenant API Key for Intaleq Maps SaaS',
required: true,
}),
(0, common_1.Controller)('tactical'),
(0, common_1.UseGuards)(api_key_guard_1.ApiKeyGuard, rate_limiter_guard_1.TenantThrottlerGuard),
__metadata("design:paramtypes", [tactical_service_1.TacticalService])
], TacticalController);
//# sourceMappingURL=tactical.controller.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"tactical.controller.js","sourceRoot":"","sources":["../../src/tactical/tactical.controller.ts"],"names":[],"mappings":"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"}
+2
View File
@@ -0,0 +1,2 @@
export declare class TacticalModule {
}
+25
View File
@@ -0,0 +1,25 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TacticalModule = void 0;
const common_1 = require("@nestjs/common");
const tactical_controller_1 = require("./tactical.controller");
const tactical_service_1 = require("./tactical.service");
const redis_module_1 = require("../common/redis.module");
let TacticalModule = class TacticalModule {
};
exports.TacticalModule = TacticalModule;
exports.TacticalModule = TacticalModule = __decorate([
(0, common_1.Module)({
imports: [redis_module_1.RedisModule],
controllers: [tactical_controller_1.TacticalController],
providers: [tactical_service_1.TacticalService],
exports: [tactical_service_1.TacticalService],
})
], TacticalModule);
//# sourceMappingURL=tactical.module.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"tactical.module.js","sourceRoot":"","sources":["../../src/tactical/tactical.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,+DAA2D;AAC3D,yDAAqD;AACrD,yDAAqD;AAQ9C,IAAM,cAAc,GAApB,MAAM,cAAc;CAAG,CAAA;AAAjB,wCAAc;yBAAd,cAAc;IAN1B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,0BAAW,CAAC;QACtB,WAAW,EAAE,CAAC,wCAAkB,CAAC;QACjC,SAAS,EAAE,CAAC,kCAAe,CAAC;QAC5B,OAAO,EAAE,CAAC,kCAAe,CAAC;KAC3B,CAAC;GACW,cAAc,CAAG"}
+69
View File
@@ -0,0 +1,69 @@
import { RedisService } from '../common/redis.service';
export interface ArtilleryMissionRequest {
gunLat: number;
gunLng: number;
targetLat: number;
targetLng: number;
gunElevationOffset?: number;
targetElevationOffset?: number;
chargeType?: string;
muzzleVelocity?: number;
caliber?: string;
}
export interface TacticalSymbol {
id: string;
type: 'friendly' | 'hostile' | 'neutral' | 'unknown' | 'radar' | 'artillery' | 'minefield' | 'checkpoint' | 'hlz' | 'op';
name: string;
name_ar: string;
lat: number;
lng: number;
elevation?: number;
notes?: string;
timeAdded: string;
}
export declare class TacticalService {
private readonly redisService;
private readonly logger;
constructor(redisService: RedisService);
calculateArtilleryFireMission(dto: ArtilleryMissionRequest): Promise<{
fireMissionId: string;
caliber: string;
muzzleVelocityMs: number;
distanceMeters: number;
distanceKm: number;
azimuthDegrees: number;
azimuthMils: number;
gunElevationMeters: number;
targetElevationMeters: number;
apexAltitudeMeters: number;
timeOfFlightSeconds: number;
lowAngle: {
degrees: number;
mils: number;
};
highAngle: {
degrees: number;
mils: number;
};
hasCrestClearance: boolean;
criticalObstacle: any;
trajectoryPoints: any[];
}>;
assessHelicopterLandingZones(lat: number, lng: number, radiusMeters?: number): Promise<{
centerLat: number;
centerLng: number;
searchRadiusMeters: number;
totalAssessed: number;
suitableCount: number;
zones: any[];
}>;
saveScenario(name: string, symbols: TacticalSymbol[]): Promise<{
success: boolean;
count: number;
name: string;
}>;
getScenario(name: string): Promise<TacticalSymbol[]>;
private haversineDistance;
private calculateBearing;
private estimateElevation;
}
+199
View File
@@ -0,0 +1,199 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var TacticalService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TacticalService = void 0;
const common_1 = require("@nestjs/common");
const redis_service_1 = require("../common/redis.service");
let TacticalService = TacticalService_1 = class TacticalService {
redisService;
logger = new common_1.Logger(TacticalService_1.name);
constructor(redisService) {
this.redisService = redisService;
}
async calculateArtilleryFireMission(dto) {
const { gunLat, gunLng, targetLat, targetLng, gunElevationOffset = 2, targetElevationOffset = 0, muzzleVelocity = 827, caliber = '155mm Howitzer' } = dto;
const distanceMeters = this.haversineDistance(gunLat, gunLng, targetLat, targetLng);
const azimuthDegrees = this.calculateBearing(gunLat, gunLng, targetLat, targetLng);
const gunGroundElev = this.estimateElevation(gunLat, gunLng);
const targetGroundElev = this.estimateElevation(targetLat, targetLng);
const gunTotalElev = gunGroundElev + gunElevationOffset;
const targetTotalElev = targetGroundElev + targetElevationOffset;
const heightDiff = targetTotalElev - gunTotalElev;
const g = 9.80665;
const v0 = muzzleVelocity;
const term = Math.pow(v0, 4) - g * (g * Math.pow(distanceMeters, 2) + 2 * heightDiff * Math.pow(v0, 2));
let lowAngleRad = 0;
let highAngleRad = 0;
let maxRange = (Math.pow(v0, 2) / g);
let isInRange = term >= 0 && distanceMeters <= maxRange;
if (isInRange) {
const sqrtTerm = Math.sqrt(term);
lowAngleRad = Math.atan((Math.pow(v0, 2) - sqrtTerm) / (g * distanceMeters));
highAngleRad = Math.atan((Math.pow(v0, 2) + sqrtTerm) / (g * distanceMeters));
}
else {
lowAngleRad = (45 * Math.PI) / 180;
highAngleRad = (60 * Math.PI) / 180;
}
const lowAngleDeg = (lowAngleRad * 180) / Math.PI;
const lowAngleMils = (lowAngleDeg * (6400 / 360));
const highAngleDeg = (highAngleRad * 180) / Math.PI;
const highAngleMils = (highAngleDeg * (6400 / 360));
const timeOfFlightSeconds = distanceMeters / (v0 * Math.cos(lowAngleRad));
const apexHeightMeters = gunTotalElev + (Math.pow(v0 * Math.sin(lowAngleRad), 2) / (2 * g));
const trajectoryPoints = [];
const samples = 50;
let hasCrestClearance = true;
let criticalObstacle = null;
for (let i = 0; i <= samples; i++) {
const frac = i / samples;
const d = distanceMeters * frac;
const lat = gunLat + (targetLat - gunLat) * frac;
const lng = gunLng + (targetLng - gunLng) * frac;
const t = frac * timeOfFlightSeconds;
const y = (v0 * Math.sin(lowAngleRad) * t) - (0.5 * g * Math.pow(t, 2));
const projectileAlt = gunTotalElev + y;
const terrainElev = this.estimateElevation(lat, lng);
const clearance = projectileAlt - terrainElev;
if (clearance <= 0 && i > 1 && i < samples) {
hasCrestClearance = false;
if (!criticalObstacle || clearance < criticalObstacle.clearance) {
criticalObstacle = {
distanceMeters: Math.round(d),
terrainElevMeters: Math.round(terrainElev),
projectileAltMeters: Math.round(projectileAlt),
deficitMeters: Math.round(Math.abs(clearance)),
lat,
lng
};
}
}
trajectoryPoints.push({
distanceMeters: Math.round(d),
lat,
lng,
terrainElevation: Math.round(terrainElev),
projectileAltitude: Math.round(projectileAlt),
clearanceMeters: Math.round(clearance)
});
}
return {
fireMissionId: `FM-${Date.now().toString().slice(-6)}`,
caliber,
muzzleVelocityMs: muzzleVelocity,
distanceMeters: Math.round(distanceMeters),
distanceKm: Math.round((distanceMeters / 1000) * 100) / 100,
azimuthDegrees: Math.round(azimuthDegrees * 10) / 10,
azimuthMils: Math.round((azimuthDegrees * (6400 / 360)) * 10) / 10,
gunElevationMeters: Math.round(gunTotalElev),
targetElevationMeters: Math.round(targetTotalElev),
apexAltitudeMeters: Math.round(apexHeightMeters),
timeOfFlightSeconds: Math.round(timeOfFlightSeconds * 10) / 10,
lowAngle: {
degrees: Math.round(lowAngleDeg * 100) / 100,
mils: Math.round(lowAngleMils * 10) / 10,
},
highAngle: {
degrees: Math.round(highAngleDeg * 100) / 100,
mils: Math.round(highAngleMils * 10) / 10,
},
hasCrestClearance,
criticalObstacle,
trajectoryPoints
};
}
async assessHelicopterLandingZones(lat, lng, radiusMeters = 3000) {
const candidates = [];
const samples = 16;
for (let i = 0; i < samples; i++) {
const angle = (i * 2 * Math.PI) / samples;
const r = (radiusMeters * 0.3) + Math.random() * (radiusMeters * 0.6);
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);
const hlzLat = lat + dLat;
const hlzLng = lng + dLng;
const centerElev = this.estimateElevation(hlzLat, hlzLng);
const northElev = this.estimateElevation(hlzLat + 0.0005, hlzLng);
const eastElev = this.estimateElevation(hlzLat, hlzLng + 0.0005);
const slopeDeg = Math.round(Math.atan(Math.max(Math.abs(northElev - centerElev), Math.abs(eastElev - centerElev)) / 50) * (180 / Math.PI) * 10) / 10;
const isSuitable = slopeDeg <= 7.0;
candidates.push({
id: `HLZ-${i + 1}`,
lat: hlzLat,
lng: hlzLng,
elevationMeters: Math.round(centerElev),
distanceMeters: Math.round(r),
slopeDegrees: slopeDeg,
suitability: isSuitable ? 'EXCELLENT' : slopeDeg <= 12 ? 'MARGINAL' : 'UNSUITABLE',
suitability_ar: isSuitable ? 'ممتاز (مستوي وخالي من العوائق)' : slopeDeg <= 12 ? 'مقبول بحذر' : 'غير صالح (شديد الانحدار)',
maxRotorDiameterMeters: isSuitable ? 25 : 15,
windApproachBearingDeg: Math.round(Math.random() * 360)
});
}
return {
centerLat: lat,
centerLng: lng,
searchRadiusMeters: radiusMeters,
totalAssessed: candidates.length,
suitableCount: candidates.filter(c => c.suitability === 'EXCELLENT').length,
zones: candidates
};
}
async saveScenario(name, symbols) {
const key = `tactical:scenario:${name}`;
await this.redisService.set(key, JSON.stringify(symbols), 86400 * 7);
return { success: true, count: symbols.length, name };
}
async getScenario(name) {
const key = `tactical:scenario:${name}`;
const data = await this.redisService.get(key);
return data || [];
}
haversineDistance(lat1, lon1, lat2, lon2) {
const R = 6371000;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
calculateBearing(lat1, lon1, lat2, lon2) {
const phi1 = (lat1 * Math.PI) / 180;
const phi2 = (lat2 * Math.PI) / 180;
const deltaLambda = ((lon2 - lon1) * Math.PI) / 180;
const y = Math.sin(deltaLambda) * Math.cos(phi2);
const x = Math.cos(phi1) * Math.sin(phi2) - Math.sin(phi1) * Math.cos(phi2) * Math.cos(deltaLambda);
return (Math.atan2(y, x) * (180 / Math.PI) + 360) % 360;
}
estimateElevation(lat, lng) {
if (lng < 35.6 && lat < 32.2 && lat > 31.0) {
return -400 + Math.abs(lng - 35.5) * 3000;
}
if (lat >= 32.1 && lng < 36.0) {
return 850 + Math.sin(lat * 50) * 250 + Math.cos(lng * 40) * 150;
}
if (lat >= 31.8 && lat < 32.1 && lng >= 35.8 && lng < 36.2) {
return 900 + Math.sin((lat - 31.95) * 100) * 120 + Math.cos((lng - 35.9) * 100) * 100;
}
if (lat < 31.5 && lat > 30.0 && lng < 35.7) {
return 1100 + Math.sin(lat * 30) * 350;
}
return 650 + (lng - 36.0) * 30;
}
};
exports.TacticalService = TacticalService;
exports.TacticalService = TacticalService = TacticalService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [redis_service_1.RedisService])
], TacticalService);
//# sourceMappingURL=tactical.service.js.map
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
import { ConfigService } from '@nestjs/config';
export declare class ExternalTelemetryService {
private configService;
private readonly logger;
private readonly baseUrl;
private readonly apiKey;
constructor(configService: ConfigService);
fetchCarTracks(days?: number): Promise<any[]>;
}
-69
View File
@@ -1,69 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var ExternalTelemetryService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExternalTelemetryService = void 0;
const common_1 = require("@nestjs/common");
const config_1 = require("@nestjs/config");
const axios_1 = __importDefault(require("axios"));
let ExternalTelemetryService = ExternalTelemetryService_1 = class ExternalTelemetryService {
configService;
logger = new common_1.Logger(ExternalTelemetryService_1.name);
baseUrl;
apiKey;
constructor(configService) {
this.configService = configService;
this.baseUrl = this.configService.get('LOCATION_SERVER_URL', '');
this.apiKey = this.configService.get('LOCATION_SERVER_API_KEY', '');
}
async fetchCarTracks(days = 1) {
if (!this.baseUrl || !this.apiKey) {
this.logger.error('LOCATION_SERVER_URL or API_KEY not configured in .env');
return [];
}
try {
this.logger.log(`📡 Fetching car tracks from ${this.baseUrl} (Scanning: ${days} days)`);
const response = await axios_1.default.get(`${this.baseUrl}/intaleq/ride/location/getDrirversLocationsTrack.php`, {
params: { days },
headers: {
'X-API-KEY': this.apiKey,
},
timeout: 30000,
});
if (response.data && response.data.success && Array.isArray(response.data.data)) {
this.logger.log(`✅ Successfully fetched ${response.data.data.length} telemetry points.`);
return response.data.data;
}
else {
this.logger.warn('⚠️ Received invalid data structure from location server.');
return [];
}
}
catch (error) {
if (error.response) {
this.logger.error(`❌ Failed to fetch car tracks. Server returned Status ${error.response.status}: ${JSON.stringify(error.response.data)}`);
}
else {
this.logger.error(`❌ Failed to fetch car tracks: ${error.message}`);
}
return [];
}
}
};
exports.ExternalTelemetryService = ExternalTelemetryService;
exports.ExternalTelemetryService = ExternalTelemetryService = ExternalTelemetryService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [config_1.ConfigService])
], ExternalTelemetryService);
//# sourceMappingURL=external-telemetry.service.js.map
@@ -1 +0,0 @@
{"version":3,"file":"external-telemetry.service.js","sourceRoot":"","sources":["../../src/telemetry/external-telemetry.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAAoD;AACpD,2CAA+C;AAC/C,kDAA0B;AASnB,IAAM,wBAAwB,gCAA9B,MAAM,wBAAwB;IAKf;IAJH,MAAM,GAAG,IAAI,eAAM,CAAC,0BAAwB,CAAC,IAAI,CAAC,CAAC;IACnD,OAAO,CAAS;IAChB,MAAM,CAAS;IAEhC,YAAoB,aAA4B;QAA5B,kBAAa,GAAb,aAAa,CAAe;QAC9C,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAS,qBAAqB,EAAE,EAAE,CAAC,CAAC;QACzE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,aAAa,CAAC,GAAG,CAAS,yBAAyB,EAAE,EAAE,CAAC,CAAC;IAC9E,CAAC;IAMD,KAAK,CAAC,cAAc,CAAC,OAAe,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAClC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uDAAuD,CAAC,CAAC;YAC3E,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,IAAI,CAAC;YACH,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,+BAA+B,IAAI,CAAC,OAAO,eAAe,IAAI,QAAQ,CAAC,CAAC;YAExF,MAAM,QAAQ,GAAG,MAAM,eAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,OAAO,sDAAsD,EAAE;gBACtG,MAAM,EAAE,EAAE,IAAI,EAAE;gBAChB,OAAO,EAAE;oBACP,WAAW,EAAE,IAAI,CAAC,MAAM;iBACzB;gBACD,OAAO,EAAE,KAAK;aACf,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAChF,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,0BAA0B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,oBAAoB,CAAC,CAAC;gBACzF,OAAO,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;YAC5B,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,0DAA0D,CAAC,CAAC;gBAC7E,OAAO,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACnB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wDAAwD,KAAK,CAAC,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC7I,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,iCAAiC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;YACtE,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;CACF,CAAA;AA/CY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,mBAAU,GAAE;qCAMwB,sBAAa;GALrC,wBAAwB,CA+CpC"}
-40
View File
@@ -1,40 +0,0 @@
import { TelemetryAnalyzerService } from './telemetry-analyzer.service';
export declare class MapRefinementController {
private readonly analyzerService;
constructor(analyzerService: TelemetryAnalyzerService);
getSummary(): Promise<{
telemetry: {
total: any;
};
roads: {
analyzed: any;
closed: any;
};
candidates: {
total: any;
pending: any;
};
}>;
syncExternal(): Promise<void>;
refreshCache(): Promise<{
cachedCount: any;
}>;
analyzeRoadSpeeds(hours?: number): Promise<any>;
discoverNewRoads(hours?: number): Promise<any>;
getCandidates(status?: string): Promise<import("../maps/candidate-road.entity").CandidateRoad[]>;
discoverClosures(hours?: number): Promise<{
roadsClosed: any;
}>;
getClosures(): Promise<import("../maps/road-stat.entity").RoadSegmentStat[]>;
getCongestionData(north: number, south: number, east: number, west: number): Promise<any>;
approveCandidate(id: string): Promise<{
success: boolean;
id: string;
status: string;
}>;
rejectCandidate(id: string): Promise<{
success: boolean;
id: string;
status: string;
}>;
}
-171
View File
@@ -1,171 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MapRefinementController = void 0;
const common_1 = require("@nestjs/common");
const swagger_1 = require("@nestjs/swagger");
const telemetry_analyzer_service_1 = require("./telemetry-analyzer.service");
const api_key_guard_1 = require("../common/guards/api-key.guard");
let MapRefinementController = class MapRefinementController {
analyzerService;
constructor(analyzerService) {
this.analyzerService = analyzerService;
}
async getSummary() {
return this.analyzerService.getAnalysisSummary();
}
async syncExternal() {
return this.analyzerService.syncExternalData();
}
async refreshCache() {
return this.analyzerService.refreshTrafficCache();
}
async analyzeRoadSpeeds(hours) {
const sinceHours = hours ? Number(hours) : 24;
return this.analyzerService.analyzeRoadSpeeds(sinceHours);
}
async discoverNewRoads(hours) {
const sinceHours = hours ? Number(hours) : 168;
return this.analyzerService.discoverNewRoads(sinceHours);
}
async getCandidates(status) {
return this.analyzerService.getCandidates(status || 'pending');
}
async discoverClosures(hours) {
return this.analyzerService.discoverRoadClosures(hours || 48);
}
async getClosures() {
return this.analyzerService.getClosures();
}
async getCongestionData(north, south, east, west) {
if (!north || !south || !east || !west) {
throw new common_1.HttpException('Bounds required: north, south, east, west / يجب تحديد الحدود الجغرافية', common_1.HttpStatus.BAD_REQUEST);
}
return this.analyzerService.getCongestionData({
north: Number(north),
south: Number(south),
east: Number(east),
west: Number(west),
});
}
async approveCandidate(id) {
return this.analyzerService.updateCandidateStatus(id, 'approved');
}
async rejectCandidate(id) {
return this.analyzerService.updateCandidateStatus(id, 'rejected');
}
};
exports.MapRefinementController = MapRefinementController;
__decorate([
(0, common_1.Get)('summary'),
(0, swagger_1.ApiOperation)({ summary: 'Get analysis summary 📊 / ملخص التحليل' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "getSummary", null);
__decorate([
(0, common_1.Post)('sync-external'),
(0, swagger_1.ApiOperation)({ summary: 'Sync telemetry from external server 📡 / مزامنة البيانات الخارجية' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "syncExternal", null);
__decorate([
(0, common_1.Post)('refresh-cache'),
(0, swagger_1.ApiOperation)({ summary: 'Refresh Redis traffic snapshot 🚀 / تحديث ذاكرة التخزين' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "refreshCache", null);
__decorate([
(0, common_1.Post)('analyze-speeds'),
(0, swagger_1.ApiOperation)({ summary: 'Run road speed analysis 🚗 / تشغيل تحليل سرعات الطرق' }),
(0, swagger_1.ApiQuery)({ name: 'hours', required: false, description: 'Hours to look back (default: 24)' }),
__param(0, (0, common_1.Query)('hours')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "analyzeRoadSpeeds", null);
__decorate([
(0, common_1.Post)('discover-roads'),
(0, swagger_1.ApiOperation)({ summary: 'Discover new roads from telemetry 🛣️ / اكتشاف طرق جديدة' }),
(0, swagger_1.ApiQuery)({ name: 'hours', required: false, description: 'Hours to look back (default: 168 = 1 week)' }),
__param(0, (0, common_1.Query)('hours')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "discoverNewRoads", null);
__decorate([
(0, common_1.Get)('candidates'),
(0, swagger_1.ApiOperation)({ summary: 'Get list of candidate roads / جلب الطرق المرشحة' }),
(0, swagger_1.ApiQuery)({ name: 'status', required: false, enum: ['pending', 'approved', 'rejected'] }),
__param(0, (0, common_1.Query)('status')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "getCandidates", null);
__decorate([
(0, common_1.Post)('discover-closures'),
(0, swagger_1.ApiOperation)({ summary: 'Run road closure detection 🚧 / تشغيل كشف الإغلاقات' }),
(0, swagger_1.ApiQuery)({ name: 'hours', required: false, description: 'Hours to look back (default: 48)' }),
__param(0, (0, common_1.Query)('hours')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "discoverClosures", null);
__decorate([
(0, common_1.Get)('closures'),
(0, swagger_1.ApiOperation)({ summary: 'Get currently detected closures / جلب الإغلاقات المكتشفة' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "getClosures", null);
__decorate([
(0, common_1.Get)('congestion'),
(0, swagger_1.ApiOperation)({ summary: 'Get congestion data for heatmap 🗺️ / بيانات الازدحام' }),
(0, swagger_1.ApiQuery)({ name: 'north', required: true }),
(0, swagger_1.ApiQuery)({ name: 'south', required: true }),
(0, swagger_1.ApiQuery)({ name: 'east', required: true }),
(0, swagger_1.ApiQuery)({ name: 'west', required: true }),
__param(0, (0, common_1.Query)('north')),
__param(1, (0, common_1.Query)('south')),
__param(2, (0, common_1.Query)('east')),
__param(3, (0, common_1.Query)('west')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Number, Number, Number]),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "getCongestionData", null);
__decorate([
(0, common_1.Patch)('candidates/:id/approve'),
(0, swagger_1.ApiOperation)({ summary: 'Approve a candidate road ✅ / الموافقة على طريق مرشح' }),
__param(0, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "approveCandidate", null);
__decorate([
(0, common_1.Patch)('candidates/:id/reject'),
(0, swagger_1.ApiOperation)({ summary: 'Reject a candidate road ❌ / رفض طريق مرشح' }),
__param(0, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], MapRefinementController.prototype, "rejectCandidate", null);
exports.MapRefinementController = MapRefinementController = __decorate([
(0, swagger_1.ApiTags)('map-refinement'),
(0, common_1.Controller)('map-refinement'),
(0, common_1.UseGuards)(api_key_guard_1.ApiKeyGuard),
__metadata("design:paramtypes", [telemetry_analyzer_service_1.TelemetryAnalyzerService])
], MapRefinementController);
//# sourceMappingURL=map-refinement.controller.js.map
@@ -1 +0,0 @@
{"version":3,"file":"map-refinement.controller.js","sourceRoot":"","sources":["../../src/telemetry/map-refinement.controller.ts"],"names":[],"mappings":"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"}
-61
View File
@@ -1,61 +0,0 @@
import { Repository, DataSource } from 'typeorm';
import { TelemetryLog } from './telemetry.entity';
import { RoadSegmentStat } from '../maps/road-stat.entity';
import { CandidateRoad } from '../maps/candidate-road.entity';
import { ExternalTelemetryService } from './external-telemetry.service';
import { RedisService } from '../common/redis.service';
import { TelegramService } from '../common/telegram.service';
import { TrafficGridService } from '../maps/traffic-grid.service';
export declare class TelemetryAnalyzerService {
private telemetryRepo;
private roadStatRepo;
private candidateRoadRepo;
private externalTelemetry;
private redisService;
private telegramService;
private trafficGrid;
private dataSource;
private readonly logger;
private readonly TRAFFIC_CACHE_KEY;
constructor(telemetryRepo: Repository<TelemetryLog>, roadStatRepo: Repository<RoadSegmentStat>, candidateRoadRepo: Repository<CandidateRoad>, externalTelemetry: ExternalTelemetryService, redisService: RedisService, telegramService: TelegramService, trafficGrid: TrafficGridService, dataSource: DataSource);
runDeepIntelligence(hours?: number): Promise<{
success: boolean;
timestamp: string;
speedAnalysis: any;
temporalAnalysis: any;
roadDiscovery: any;
}>;
handleDeepIntelligence(): void;
syncExternalData(days?: number): Promise<void>;
refreshTrafficCache(): Promise<{
cachedCount: any;
}>;
analyzeRoadSpeeds(sinceHours?: number): Promise<any>;
analyzeTimeProfiles(sinceHours?: number): Promise<any>;
discoverNewRoads(sinceHours?: number): Promise<any>;
getCongestionData(bounds: any): Promise<any>;
getAnalysisSummary(): Promise<{
telemetry: {
total: any;
};
roads: {
analyzed: any;
closed: any;
};
candidates: {
total: any;
pending: any;
};
}>;
getCandidates(status?: string, limit?: number): Promise<CandidateRoad[]>;
updateCandidateStatus(id: string, status: string): Promise<{
success: boolean;
id: string;
status: string;
}>;
discoverRoadClosures(sinceHours?: number): Promise<{
roadsClosed: any;
}>;
getClosures(): Promise<RoadSegmentStat[]>;
private calculateConfidence;
}
-419
View File
@@ -1,419 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var TelemetryAnalyzerService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TelemetryAnalyzerService = void 0;
const common_1 = require("@nestjs/common");
const schedule_1 = require("@nestjs/schedule");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const telemetry_entity_1 = require("./telemetry.entity");
const road_stat_entity_1 = require("../maps/road-stat.entity");
const candidate_road_entity_1 = require("../maps/candidate-road.entity");
const external_telemetry_service_1 = require("./external-telemetry.service");
const redis_service_1 = require("../common/redis.service");
const telegram_service_1 = require("../common/telegram.service");
const traffic_grid_service_1 = require("../maps/traffic-grid.service");
let TelemetryAnalyzerService = TelemetryAnalyzerService_1 = class TelemetryAnalyzerService {
telemetryRepo;
roadStatRepo;
candidateRoadRepo;
externalTelemetry;
redisService;
telegramService;
trafficGrid;
dataSource;
logger = new common_1.Logger(TelemetryAnalyzerService_1.name);
TRAFFIC_CACHE_KEY = 'live_traffic_congested';
constructor(telemetryRepo, roadStatRepo, candidateRoadRepo, externalTelemetry, redisService, telegramService, trafficGrid, dataSource) {
this.telemetryRepo = telemetryRepo;
this.roadStatRepo = roadStatRepo;
this.candidateRoadRepo = candidateRoadRepo;
this.externalTelemetry = externalTelemetry;
this.redisService = redisService;
this.telegramService = telegramService;
this.trafficGrid = trafficGrid;
this.dataSource = dataSource;
}
async runDeepIntelligence(hours = 48) {
this.logger.log(`🚀 Starting Full Intelligence Pipeline (Window: ${hours}h)...`);
await this.syncExternalData(Math.ceil(hours / 24));
const speedResult = await this.analyzeRoadSpeeds(hours);
const temporalResult = await this.analyzeTimeProfiles(hours);
const discoveryResult = await this.discoverNewRoads(hours);
await this.refreshTrafficCache();
await this.trafficGrid.refreshGrid();
const summary = await this.getAnalysisSummary();
await this.telegramService.sendIntelligenceReport({
syncResult: speedResult.totalPointsProcessed || 0,
updatedSegments: speedResult.segmentsUpdated || 0,
discoveredRoads: discoveryResult.candidatesFound || 0,
timeProfiles: temporalResult.bucketsUpdated || 0,
totalPoints: summary.telemetry.total,
days: Math.ceil(hours / 24),
});
this.logger.log('🏁 Intelligence Pipeline Finished Successfully.');
return {
success: true,
timestamp: new Date().toISOString(),
speedAnalysis: speedResult,
temporalAnalysis: temporalResult,
roadDiscovery: discoveryResult,
};
}
handleDeepIntelligence() {
this.runDeepIntelligence(240);
}
async syncExternalData(days = 7) {
this.logger.log(`📡 Starting batch sync (Window: ${days} days)...`);
try {
const tracks = await this.externalTelemetry.fetchCarTracks(days);
if (tracks.length === 0)
return;
this.logger.log(`📥 Saving ${tracks.length} points to database...`);
const values = tracks
.filter(t => t.driver_id && t.longitude && t.latitude)
.map(t => `('${t.driver_id}', ST_SetSRID(ST_Point(${t.longitude}, ${t.latitude}), 4326), ${t.latitude}, ${t.longitude}, ${t.speed || 0}, ${t.heading || 0}, '${t.created_at}')`).join(',');
await this.dataSource.query(`
INSERT INTO telemetry_logs ("driverId", location, latitude, longitude, speed, heading, timestamp)
VALUES ${values}
ON CONFLICT DO NOTHING
`);
this.logger.log(`✅ Batch sync complete.`);
}
catch (error) {
this.logger.error(`❌ Sync failed: ${error.message}`);
throw error;
}
}
async refreshTrafficCache() {
this.logger.log('🚀 Refreshing Redis traffic snapshot (v2.5.2 Optimized)...');
const congested = await this.roadStatRepo.query(`
SELECT "segmentId", "congestionFactor", ST_AsGeoJSON(geometry, 5) as geojson
FROM road_segment_stats
WHERE "congestionFactor" > 1.1
ORDER BY "congestionFactor" DESC
LIMIT 2000
`);
if (congested.length === 0) {
await this.redisService.del(this.TRAFFIC_CACHE_KEY);
return { cachedCount: 0 };
}
const snapshot = congested.map(row => ({
sid: row.segmentId,
cf: parseFloat(row.congestionFactor),
geo: JSON.parse(row.geojson)
}));
const sampleSize = Math.min(congested.length, 3);
const topSegments = congested.slice(0, sampleSize).map(s => `${s.segmentId} (F: ${s.congestionFactor})`).join(', ');
const totalSizeKB = Math.round(JSON.stringify(snapshot).length / 1024);
this.logger.log(`📊 Traffic Snapshot Sample (Top ${sampleSize}): ${topSegments}`);
this.logger.log(`📦 Redis Payload Size: ~${totalSizeKB} KB`);
await this.redisService.set(this.TRAFFIC_CACHE_KEY, snapshot);
this.logger.log(`✅ Redis traffic snapshot updated with ${congested.length} segments.`);
return { cachedCount: congested.length };
}
async analyzeRoadSpeeds(sinceHours = 24) {
this.logger.log(`🔍 Starting road speed analysis v2.5 for last ${sinceHours}h...`);
const query = `
WITH grid_points AS (
-- Group by 5m grid cell first to reduce spatial join volume (v2.5.1 WoW Performance)
SELECT ST_SnapToGrid(ST_Transform(location::geometry, 3857), 5) AS loc,
AVG(speed) as speed
FROM telemetry_logs
WHERE timestamp >= NOW() - INTERVAL '${sinceHours} hours' AND speed > 2
GROUP BY loc
),
matches AS (
-- Bulk Spatial Join (20m radius) with GIST optimization
SELECT l.osm_id::text as id, l.name, l.highway, g.speed, l.way
FROM grid_points g
INNER JOIN planet_osm_line l ON
l.highway IS NOT NULL AND
l.way && ST_Expand(g.loc, 20) AND
ST_DWithin(l.way, g.loc, 20)
),
stats AS (
-- Calculate median speed and aggregate samples
SELECT id, name, highway,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY speed) as avg_speed,
COUNT(*) as samples,
ST_Transform(MIN(way), 4326) as way4326
FROM matches
GROUP BY id, name, highway
HAVING COUNT(*) >= 3
)
INSERT INTO road_segment_stats ("segmentId", "averageSpeed", "sampleCount", "lastUpdated", geometry, "congestionFactor")
SELECT id, avg_speed, samples, NOW(), way4326,
CASE WHEN highway IN ('primary','secondary','trunk','motorway') AND avg_speed < 30
THEN 30/GREATEST(avg_speed,1) ELSE 1.0 END
FROM stats
ON CONFLICT ("segmentId") DO UPDATE SET
"averageSpeed"=EXCLUDED."averageSpeed",
"sampleCount"=EXCLUDED."sampleCount",
"lastUpdated"=NOW(),
"geometry"=EXCLUDED."geometry",
"congestionFactor"=EXCLUDED."congestionFactor"
RETURNING "segmentId";
`;
const result = await this.dataSource.query(query);
return { segmentsUpdated: result.length };
}
async analyzeTimeProfiles(sinceHours = 720) {
this.logger.log(`🕒 Starting temporal profiling (Phase 2) for last ${sinceHours}h...`);
const query = `
WITH grid_points AS (
-- Bucket by 10m grid and Time (Hour + DOW)
SELECT
ST_SnapToGrid(ST_Transform(location::geometry, 3857), 10) AS loc,
EXTRACT(HOUR FROM timestamp)::int as hr,
EXTRACT(DOW FROM timestamp)::int as dow,
AVG(speed) as speed
FROM telemetry_logs
WHERE timestamp >= NOW() - INTERVAL '${sinceHours} hours' AND speed > 2
GROUP BY loc, hr, dow
),
matches AS (
-- Map to OSM segments
SELECT l.osm_id::text as sid, g.hr, g.dow, g.speed
FROM grid_points g
INNER JOIN planet_osm_line l ON
l.highway IS NOT NULL AND
l.way && ST_Expand(g.loc, 20) AND
ST_DWithin(l.way, g.loc, 20)
),
temporal_stats AS (
-- Aggregate by segment + time bucket
SELECT sid, hr, dow,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY speed) as avg_spd,
COUNT(*) as smp
FROM matches
GROUP BY sid, hr, dow
HAVING COUNT(*) >= 2 -- Require minimum samples per bucket
)
INSERT INTO road_speed_profiles ("segmentId", "hourOfDay", "dayOfWeek", "averageSpeed", "sampleCount", "lastUpdated")
SELECT sid, hr, dow, avg_spd, smp, NOW()
FROM temporal_stats
ON CONFLICT ("segmentId", "hourOfDay", "dayOfWeek") DO UPDATE SET
"averageSpeed" = EXCLUDED."averageSpeed",
"sampleCount" = EXCLUDED."sampleCount",
"lastUpdated" = NOW()
RETURNING "segmentId";
`;
const result = await this.dataSource.query(query);
this.logger.log(`📊 Time-aware profiling complete: ${result.length} buckets updated.`);
return { bucketsUpdated: result.length };
}
async discoverNewRoads(sinceHours = 168) {
this.logger.log(`🛣️ Starting road discovery v2.5 for last ${sinceHours}h...`);
const query = `
WITH grid_points AS (
-- Group by cell and driver to reduce volume for clustering and anti-join (v2.5.1 WoW Performance)
SELECT "driverId", ST_SnapToGrid(ST_Transform(location::geometry, 3857), 10) as loc,
AVG(speed) as speed,
MIN(timestamp) as timestamp
FROM telemetry_logs
WHERE timestamp >= NOW() - INTERVAL '${sinceHours} hours' AND speed > 5
GROUP BY loc, "driverId"
),
off_road AS (
-- Spatial Anti-Join: Only points further than 35m from any existing highway
SELECT g.* FROM grid_points g
LEFT JOIN planet_osm_line l ON
l.highway IS NOT NULL AND
l.way && ST_Expand(g.loc, 35) AND
ST_DWithin(l.way, g.loc, 35)
WHERE l.osm_id IS NULL
),
clustered AS (
-- Density-based clustering to find linear paths
SELECT *, ST_ClusterDBSCAN(loc, eps := 30, minpoints := 3) OVER () as cid FROM off_road
),
cluster_stats AS (
-- 1. Calculate cluster-wide quality metrics
SELECT cid,
COUNT(DISTINCT "driverId") as drv_count,
COUNT(*) as pt_count,
AVG(speed) as avg_spd
FROM clustered
WHERE cid IS NOT NULL
GROUP BY cid
HAVING COUNT(DISTINCT "driverId") >= 3 AND COUNT(*) >= 15
),
cluster_path AS (
-- 2. Extract unique grid cells per cluster in chronological order
SELECT cid, loc, MIN(timestamp) as ts
FROM clustered
WHERE cid IN (SELECT cid FROM cluster_stats)
GROUP BY cid, loc
),
final_candidates AS (
-- 3. Build geometry and calculate final scoring
SELECT
s.cid,
ST_Transform(ST_MakeLine(p.loc ORDER BY p.ts), 4326) as geom,
s.drv_count,
s.pt_count,
s.avg_spd,
ST_Length(ST_Transform(ST_MakeLine(p.loc ORDER BY p.ts), 4326)::geography) as len
FROM cluster_stats s
JOIN cluster_path p ON s.cid = p.cid
GROUP BY s.cid, s.drv_count, s.pt_count, s.avg_spd
),
oneway_detect AS (
-- 4. Detect one-way direction from telemetry heading data
SELECT
fc.cid,
DEGREES(ST_Azimuth(
ST_StartPoint(fc.geom::geometry),
ST_EndPoint(fc.geom::geometry)
)) AS geom_bearing,
AVG(tl.heading) AS avg_heading,
COUNT(*) AS heading_samples
FROM final_candidates fc
JOIN telemetry_logs tl ON ST_DWithin(
tl.location::geometry,
fc.geom::geometry,
30
)
WHERE tl.speed > 3
GROUP BY fc.cid, fc.geom
HAVING COUNT(*) >= 5
),
oneway_scored AS (
SELECT
od.cid,
CASE
WHEN LEAST(ABS(od.avg_heading - od.geom_bearing), 360 - ABS(od.avg_heading - od.geom_bearing)) < 45
AND od.heading_samples >= 10 THEN 1
WHEN LEAST(ABS(od.avg_heading - (od.geom_bearing + 180) % 360), 360 - ABS(od.avg_heading - (od.geom_bearing + 180) % 360)) < 45
AND od.heading_samples >= 10 THEN -1
ELSE 0
END AS oneway_val
FROM oneway_detect od
)
INSERT INTO candidate_roads (geometry, "uniqueDriverCount", "totalPoints", "averageSpeed", "lengthMeters", confidence, status, oneway)
SELECT fc.geom, fc.drv_count, fc.pt_count, fc.avg_spd, fc.len,
ROUND((
LEAST(fc.drv_count::float / 5, 1.0) * 0.4 +
LEAST(fc.pt_count::float / 50, 1.0) * 0.3 +
CASE WHEN fc.len BETWEEN 50 AND 2000 THEN 0.3 ELSE 0.1 END
)::numeric, 2) as conf,
'pending',
COALESCE(os.oneway_val, 0)
FROM final_candidates fc
LEFT JOIN oneway_scored os ON os.cid = fc.cid
RETURNING id;
`;
const result = await this.dataSource.query(query);
return { candidatesFound: result.length };
}
async getCongestionData(bounds) {
return this.dataSource.query(`
SELECT rs."segmentId", rs."averageSpeed", rs."congestionFactor", rs."sampleCount", ST_AsGeoJSON(rs.geometry) as geojson
FROM road_segment_stats rs
WHERE rs.geometry IS NOT NULL AND ST_Intersects(rs.geometry::geometry, ST_MakeEnvelope($1, $2, $3, $4, 4326))
ORDER BY rs."congestionFactor" DESC
`, [bounds.west, bounds.south, bounds.east, bounds.north]);
}
async getAnalysisSummary() {
const [tCount] = await this.dataSource.query('SELECT COUNT(*) as count FROM telemetry_logs');
const [rCount] = await this.dataSource.query('SELECT COUNT(*) as count FROM road_segment_stats');
const [cCount] = await this.dataSource.query('SELECT COUNT(*) as count, COUNT(*) FILTER (WHERE status=\'pending\') as p FROM candidate_roads');
const [clCount] = await this.dataSource.query('SELECT COUNT(*) as count FROM road_segment_stats WHERE "isClosed" = true');
return {
telemetry: { total: tCount.count },
roads: { analyzed: rCount.count, closed: clCount.count },
candidates: { total: cCount.count, pending: cCount.p }
};
}
async getCandidates(status = 'pending', limit = 50) {
return this.candidateRoadRepo.find({
where: { status },
order: { confidence: 'DESC' },
take: limit
});
}
async updateCandidateStatus(id, status) {
await this.candidateRoadRepo.update(id, {
status,
reviewedAt: new Date()
});
return { success: true, id, status };
}
async discoverRoadClosures(sinceHours = 48) {
this.logger.log(`🚧 Analyzing road closures for last ${sinceHours}h...`);
await this.roadStatRepo.update({ isClosed: true }, { isClosed: false });
const query = `
WITH active_area AS (
-- Bounding box of some recent activity to prove drivers are on the map
SELECT ST_Expand(ST_Extent(location::geometry), 0.01) as bbox
FROM telemetry_logs
WHERE timestamp >= NOW() - INTERVAL '${sinceHours} hours'
),
possible_closures AS (
SELECT rs."segmentId"
FROM road_segment_stats rs
WHERE rs."sampleCount" > 50
AND rs.geometry && (SELECT bbox FROM active_area)
AND NOT EXISTS (
SELECT 1 FROM telemetry_logs t
WHERE t.timestamp >= NOW() - INTERVAL '${sinceHours} hours'
AND ST_DWithin(rs.geometry::geometry, t.location::geometry, 35)
)
)
UPDATE road_segment_stats
SET "isClosed" = true
WHERE "segmentId" IN (SELECT "segmentId" FROM possible_closures)
RETURNING "segmentId";
`;
const result = await this.dataSource.query(query);
this.logger.log(`✅ Road closure detection complete: ${result.length} roads flagged as closed.`);
return { roadsClosed: result.length };
}
async getClosures() {
return this.roadStatRepo.find({
where: { isClosed: true },
order: { sampleCount: 'DESC' }
});
}
calculateConfidence(drivers, points, len) {
const dScore = Math.min(drivers / 5, 1.0) * 0.4;
const pScore = Math.min(points / 50, 1.0) * 0.3;
const lScore = (len >= 50 && len <= 2000) ? 0.3 : 0.1;
return Math.round((dScore + pScore + lScore) * 100) / 100;
}
};
exports.TelemetryAnalyzerService = TelemetryAnalyzerService;
__decorate([
(0, schedule_1.Cron)('0 4 */10 * *'),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], TelemetryAnalyzerService.prototype, "handleDeepIntelligence", null);
exports.TelemetryAnalyzerService = TelemetryAnalyzerService = TelemetryAnalyzerService_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, typeorm_1.InjectRepository)(telemetry_entity_1.TelemetryLog)),
__param(1, (0, typeorm_1.InjectRepository)(road_stat_entity_1.RoadSegmentStat)),
__param(2, (0, typeorm_1.InjectRepository)(candidate_road_entity_1.CandidateRoad)),
__metadata("design:paramtypes", [typeorm_2.Repository,
typeorm_2.Repository,
typeorm_2.Repository,
external_telemetry_service_1.ExternalTelemetryService,
redis_service_1.RedisService,
telegram_service_1.TelegramService,
traffic_grid_service_1.TrafficGridService,
typeorm_2.DataSource])
], TelemetryAnalyzerService);
//# sourceMappingURL=telemetry-analyzer.service.js.map
File diff suppressed because one or more lines are too long
-40
View File
@@ -1,40 +0,0 @@
import { TelemetryService, type DriverTelemetryDto } from './telemetry.service';
import { TelemetryAnalyzerService } from './telemetry-analyzer.service';
export declare class TelemetryController {
private readonly telemetryService;
private readonly analyzerService;
constructor(telemetryService: TelemetryService, analyzerService: TelemetryAnalyzerService);
ingest(data: DriverTelemetryDto): Promise<{
success: boolean;
timestamp: Date;
}>;
sync(): Promise<void>;
analyze(hours?: number): Promise<{
success: boolean;
roadAnalysis: any;
roadDiscovery: any;
}>;
processIntelligence(hours?: string, days?: string): Promise<{
success: boolean;
message: string;
timestamp: Date;
window: {
days: number;
hours: number;
};
}>;
getSummary(): Promise<{
telemetry: {
total: any;
};
roads: {
analyzed: any;
closed: any;
};
candidates: {
total: any;
pending: any;
};
}>;
getNearby(lat: number, lng: number, radius: number): Promise<import("./telemetry.entity").TelemetryLog[]>;
}
-118
View File
@@ -1,118 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TelemetryController = void 0;
const telemetry_service_1 = require("./telemetry.service");
const telemetry_analyzer_service_1 = require("./telemetry-analyzer.service");
const api_key_guard_1 = require("../common/guards/api-key.guard");
const common_1 = require("@nestjs/common");
const swagger_1 = require("@nestjs/swagger");
let TelemetryController = class TelemetryController {
telemetryService;
analyzerService;
constructor(telemetryService, analyzerService) {
this.telemetryService = telemetryService;
this.analyzerService = analyzerService;
}
async ingest(data) {
return this.telemetryService.ingest(data);
}
async sync() {
return this.analyzerService.syncExternalData();
}
async analyze(hours) {
const roadResults = await this.analyzerService.analyzeRoadSpeeds(hours || 24);
const discoveryResults = await this.analyzerService.discoverNewRoads(hours || 168);
return {
success: true,
roadAnalysis: roadResults,
roadDiscovery: discoveryResults,
};
}
async processIntelligence(hours, days) {
const parsedDays = days ? parseInt(days, 10) : 1;
const parsedHours = hours ? parseInt(hours, 10) : (parsedDays * 24);
this.analyzerService.runDeepIntelligence(parsedHours);
return {
success: true,
message: `Intelligence sync and analysis started for the last ${parsedDays} days. You will receive a summary on Telegram once complete! 🚀`,
timestamp: new Date(),
window: { days: parsedDays, hours: parsedHours },
};
}
async getSummary() {
return this.analyzerService.getAnalysisSummary();
}
async getNearby(lat, lng, radius) {
return this.telemetryService.getRecentDrivers(Number(lat), Number(lng), Number(radius));
}
};
exports.TelemetryController = TelemetryController;
__decorate([
(0, common_1.Post)(),
(0, swagger_1.ApiOperation)({ summary: 'Ingest driver telemetry 📡' }),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], TelemetryController.prototype, "ingest", null);
__decorate([
(0, common_1.Post)('sync'),
(0, swagger_1.ApiOperation)({ summary: 'Sync data from external server 📡' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], TelemetryController.prototype, "sync", null);
__decorate([
(0, common_1.Post)('analyze'),
(0, swagger_1.ApiOperation)({ summary: 'Trigger road and speed analysis 🧠' }),
__param(0, (0, common_1.Query)('hours')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", Promise)
], TelemetryController.prototype, "analyze", null);
__decorate([
(0, common_1.Post)('process-intelligence'),
(0, swagger_1.ApiOperation)({ summary: 'Automated One-Click Sync & Analyze 🚀' }),
__param(0, (0, common_1.Query)('hours')),
__param(1, (0, common_1.Query)('days')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", Promise)
], TelemetryController.prototype, "processIntelligence", null);
__decorate([
(0, common_1.Get)('summary'),
(0, swagger_1.ApiOperation)({ summary: 'Get analysis summary 📊' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], TelemetryController.prototype, "getSummary", null);
__decorate([
(0, common_1.Get)('nearby'),
(0, swagger_1.ApiOperation)({ summary: 'Find nearby drivers 📍' }),
__param(0, (0, common_1.Query)('lat')),
__param(1, (0, common_1.Query)('lng')),
__param(2, (0, common_1.Query)('radius')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number, Number, Number]),
__metadata("design:returntype", Promise)
], TelemetryController.prototype, "getNearby", null);
exports.TelemetryController = TelemetryController = __decorate([
(0, swagger_1.ApiTags)('telemetry'),
(0, common_1.Controller)('telemetry'),
(0, common_1.UseGuards)(api_key_guard_1.ApiKeyGuard),
__metadata("design:paramtypes", [telemetry_service_1.TelemetryService,
telemetry_analyzer_service_1.TelemetryAnalyzerService])
], TelemetryController);
//# sourceMappingURL=telemetry.controller.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"telemetry.controller.js","sourceRoot":"","sources":["../../src/telemetry/telemetry.controller.ts"],"names":[],"mappings":"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"}
-10
View File
@@ -1,10 +0,0 @@
export declare class TelemetryLog {
id: number;
driverId: string;
latitude: number;
longitude: number;
speed: number;
heading: number;
timestamp: Date;
location: any;
}
-69
View File
@@ -1,69 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TelemetryLog = void 0;
const typeorm_1 = require("typeorm");
let TelemetryLog = class TelemetryLog {
id;
driverId;
latitude;
longitude;
speed;
heading;
timestamp;
location;
};
exports.TelemetryLog = TelemetryLog;
__decorate([
(0, typeorm_1.PrimaryGeneratedColumn)(),
__metadata("design:type", Number)
], TelemetryLog.prototype, "id", void 0);
__decorate([
(0, typeorm_1.Column)(),
(0, typeorm_1.Index)(),
__metadata("design:type", String)
], TelemetryLog.prototype, "driverId", void 0);
__decorate([
(0, typeorm_1.Column)('decimal', { precision: 10, scale: 7 }),
__metadata("design:type", Number)
], TelemetryLog.prototype, "latitude", void 0);
__decorate([
(0, typeorm_1.Column)('decimal', { precision: 10, scale: 7 }),
__metadata("design:type", Number)
], TelemetryLog.prototype, "longitude", void 0);
__decorate([
(0, typeorm_1.Column)('float'),
__metadata("design:type", Number)
], TelemetryLog.prototype, "speed", void 0);
__decorate([
(0, typeorm_1.Column)('float'),
__metadata("design:type", Number)
], TelemetryLog.prototype, "heading", void 0);
__decorate([
(0, typeorm_1.CreateDateColumn)(),
(0, typeorm_1.Index)(),
__metadata("design:type", Date)
], TelemetryLog.prototype, "timestamp", void 0);
__decorate([
(0, typeorm_1.Column)({
type: 'geography',
spatialFeatureType: 'Point',
srid: 4326,
nullable: true,
}),
(0, typeorm_1.Index)({ spatial: true }),
__metadata("design:type", Object)
], TelemetryLog.prototype, "location", void 0);
exports.TelemetryLog = TelemetryLog = __decorate([
(0, typeorm_1.Entity)('telemetry_logs'),
(0, typeorm_1.Index)(['driverId', 'timestamp'], { unique: true })
], TelemetryLog);
//# sourceMappingURL=telemetry.entity.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"telemetry.entity.js","sourceRoot":"","sources":["../../src/telemetry/telemetry.entity.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,qCAA0F;AAInF,IAAM,YAAY,GAAlB,MAAM,YAAY;IAEvB,EAAE,CAAS;IAIX,QAAQ,CAAS;IAGjB,QAAQ,CAAS;IAGjB,SAAS,CAAS;IAGlB,KAAK,CAAS;IAGd,OAAO,CAAS;IAIhB,SAAS,CAAO;IAWhB,QAAQ,CAAM;CACf,CAAA;AAlCY,oCAAY;AAEvB;IADC,IAAA,gCAAsB,GAAE;;wCACd;AAIX;IAFC,IAAA,gBAAM,GAAE;IACR,IAAA,eAAK,GAAE;;8CACS;AAGjB;IADC,IAAA,gBAAM,EAAC,SAAS,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;;8CAC9B;AAGjB;IADC,IAAA,gBAAM,EAAC,SAAS,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;;+CAC7B;AAGlB;IADC,IAAA,gBAAM,EAAC,OAAO,CAAC;;2CACF;AAGd;IADC,IAAA,gBAAM,EAAC,OAAO,CAAC;;6CACA;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,eAAK,GAAE;8BACG,IAAI;+CAAC;AAWhB;IAPC,IAAA,gBAAM,EAAC;QACN,IAAI,EAAE,WAAW;QACjB,kBAAkB,EAAE,OAAO;QAC3B,IAAI,EAAE,IAAI;QACV,QAAQ,EAAE,IAAI;KACf,CAAC;IACD,IAAA,eAAK,EAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;;8CACX;uBAjCH,YAAY;IAFxB,IAAA,gBAAM,EAAC,gBAAgB,CAAC;IACxB,IAAA,eAAK,EAAC,CAAC,UAAU,EAAE,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;GACtC,YAAY,CAkCxB"}
-2
View File
@@ -1,2 +0,0 @@
export declare class TelemetryModule {
}
-48
View File
@@ -1,48 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TelemetryModule = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const telemetry_entity_1 = require("./telemetry.entity");
const telemetry_service_1 = require("./telemetry.service");
const telemetry_controller_1 = require("./telemetry.controller");
const telemetry_analyzer_service_1 = require("./telemetry-analyzer.service");
const map_refinement_controller_1 = require("./map-refinement.controller");
const road_stat_entity_1 = require("../maps/road-stat.entity");
const candidate_road_entity_1 = require("../maps/candidate-road.entity");
const redis_module_1 = require("../common/redis.module");
const maps_module_1 = require("../maps/maps.module");
const external_telemetry_service_1 = require("./external-telemetry.service");
const telegram_service_1 = require("../common/telegram.service");
let TelemetryModule = class TelemetryModule {
};
exports.TelemetryModule = TelemetryModule;
exports.TelemetryModule = TelemetryModule = __decorate([
(0, common_1.Module)({
imports: [
typeorm_1.TypeOrmModule.forFeature([telemetry_entity_1.TelemetryLog, road_stat_entity_1.RoadSegmentStat, candidate_road_entity_1.CandidateRoad]),
redis_module_1.RedisModule,
maps_module_1.MapsModule,
],
controllers: [telemetry_controller_1.TelemetryController, map_refinement_controller_1.MapRefinementController],
providers: [
telemetry_service_1.TelemetryService,
telemetry_analyzer_service_1.TelemetryAnalyzerService,
external_telemetry_service_1.ExternalTelemetryService,
telegram_service_1.TelegramService
],
exports: [
telemetry_service_1.TelemetryService,
telemetry_analyzer_service_1.TelemetryAnalyzerService,
external_telemetry_service_1.ExternalTelemetryService,
telegram_service_1.TelegramService
],
})
], TelemetryModule);
//# sourceMappingURL=telemetry.module.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"telemetry.module.js","sourceRoot":"","sources":["../../src/telemetry/telemetry.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,6CAAgD;AAChD,yDAAkD;AAClD,2DAAuD;AACvD,iEAA6D;AAC7D,6EAAwE;AACxE,2EAAsE;AACtE,+DAA2D;AAC3D,yEAA8D;AAC9D,yDAAqD;AAErD,qDAAiD;AAEjD,6EAAwE;AACxE,iEAA6D;AAsBtD,IAAM,eAAe,GAArB,MAAM,eAAe;CAAG,CAAA;AAAlB,0CAAe;0BAAf,eAAe;IApB3B,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,uBAAa,CAAC,UAAU,CAAC,CAAC,+BAAY,EAAE,kCAAe,EAAE,qCAAa,CAAC,CAAC;YACxE,0BAAW;YACX,wBAAU;SACX;QACD,WAAW,EAAE,CAAC,0CAAmB,EAAE,mDAAuB,CAAC;QAC3D,SAAS,EAAE;YACT,oCAAgB;YAChB,qDAAwB;YACxB,qDAAwB;YACxB,kCAAe;SAChB;QACD,OAAO,EAAE;YACP,oCAAgB;YAChB,qDAAwB;YACxB,qDAAwB;YACxB,kCAAe;SAChB;KACF,CAAC;GACW,eAAe,CAAG"}
-19
View File
@@ -1,19 +0,0 @@
import { Repository } from 'typeorm';
import { TelemetryLog } from './telemetry.entity';
export interface DriverTelemetryDto {
driver_id: string;
latitude: number;
longitude: number;
speed: number;
heading: number;
}
export declare class TelemetryService {
private telemetryRepository;
private readonly logger;
constructor(telemetryRepository: Repository<TelemetryLog>);
ingest(data: DriverTelemetryDto): Promise<{
success: boolean;
timestamp: Date;
}>;
getRecentDrivers(lat: number, lng: number, radiusMeters?: number): Promise<TelemetryLog[]>;
}
-59
View File
@@ -1,59 +0,0 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var TelemetryService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TelemetryService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const telemetry_entity_1 = require("./telemetry.entity");
let TelemetryService = TelemetryService_1 = class TelemetryService {
telemetryRepository;
logger = new common_1.Logger(TelemetryService_1.name);
constructor(telemetryRepository) {
this.telemetryRepository = telemetryRepository;
}
async ingest(data) {
const log = this.telemetryRepository.create({
driverId: data.driver_id,
latitude: data.latitude,
longitude: data.longitude,
speed: data.speed,
heading: data.heading,
location: {
type: 'Point',
coordinates: [data.longitude, data.latitude],
},
});
await this.telemetryRepository.save(log);
this.logger.debug(`Ingested telemetry for driver ${data.driver_id}`);
return { success: true, timestamp: new Date() };
}
async getRecentDrivers(lat, lng, radiusMeters = 5000) {
return this.telemetryRepository
.createQueryBuilder('log')
.where('ST_DWithin(log.location, ST_MakePoint(:lng, :lat)::geography, :radius)')
.setParameters({ lat, lng, radius: radiusMeters })
.orderBy('log.timestamp', 'DESC')
.limit(50)
.getMany();
}
};
exports.TelemetryService = TelemetryService;
exports.TelemetryService = TelemetryService = TelemetryService_1 = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, typeorm_1.InjectRepository)(telemetry_entity_1.TelemetryLog)),
__metadata("design:paramtypes", [typeorm_2.Repository])
], TelemetryService);
//# sourceMappingURL=telemetry.service.js.map
-1
View File
@@ -1 +0,0 @@
{"version":3,"file":"telemetry.service.js","sourceRoot":"","sources":["../../src/telemetry/telemetry.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAAoD;AACpD,6CAAmD;AACnD,qCAAqC;AACrC,yDAAkD;AAW3C,IAAM,gBAAgB,wBAAtB,MAAM,gBAAgB;IAKjB;IAJO,MAAM,GAAG,IAAI,eAAM,CAAC,kBAAgB,CAAC,IAAI,CAAC,CAAC;IAE5D,YAEU,mBAA6C;QAA7C,wBAAmB,GAAnB,mBAAmB,CAA0B;IACpD,CAAC;IAEJ,KAAK,CAAC,MAAM,CAAC,IAAwB;QAGnC,MAAM,GAAG,GAAG,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC;YAC1C,QAAQ,EAAE,IAAI,CAAC,SAAS;YACxB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,QAAQ,EAAE;gBACR,IAAI,EAAE,OAAO;gBACb,WAAW,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC;aAC7C;SACF,CAAC,CAAC;QAEH,MAAM,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAIzC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,iCAAiC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;QACrE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,GAAW,EAAE,GAAW,EAAE,eAAuB,IAAI;QAG1E,OAAO,IAAI,CAAC,mBAAmB;aAC5B,kBAAkB,CAAC,KAAK,CAAC;aACzB,KAAK,CAAC,wEAAwE,CAAC;aAC/E,aAAa,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;aACjD,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC;aAChC,KAAK,CAAC,EAAE,CAAC;aACT,OAAO,EAAE,CAAC;IACf,CAAC;CACF,CAAA;AA1CY,4CAAgB;2BAAhB,gBAAgB;IAD5B,IAAA,mBAAU,GAAE;IAKR,WAAA,IAAA,0BAAgB,EAAC,+BAAY,CAAC,CAAA;qCACF,oBAAU;GAL9B,gBAAgB,CA0C5B"}
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
import { WeatherService } from './weather.service';
export declare class WeatherController {
private readonly weatherService;
constructor(weatherService: WeatherService);
getWeatherGrid(): Promise<any>;
getCitiesWeather(region?: string): Promise<any>;
getWeatherAlerts(region?: string): Promise<any[]>;
}
+65
View File
@@ -0,0 +1,65 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WeatherController = void 0;
const common_1 = require("@nestjs/common");
const swagger_1 = require("@nestjs/swagger");
const weather_service_1 = require("./weather.service");
let WeatherController = class WeatherController {
weatherService;
constructor(weatherService) {
this.weatherService = weatherService;
}
async getWeatherGrid() {
return this.weatherService.getWeatherGrid();
}
async getCitiesWeather(region) {
return this.weatherService.getAllCitiesWeather(region);
}
async getWeatherAlerts(region) {
return this.weatherService.getAlerts(region);
}
};
exports.WeatherController = WeatherController;
__decorate([
(0, common_1.Get)('grid'),
(0, swagger_1.ApiOperation)({ summary: 'Get weather temperature grid (Jordan, Syria, Egypt)' }),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], WeatherController.prototype, "getWeatherGrid", null);
__decorate([
(0, common_1.Get)('cities'),
(0, swagger_1.ApiOperation)({ summary: 'Get current weather and 7-day forecast for cities' }),
(0, swagger_1.ApiQuery)({ name: 'region', required: false, description: 'Region/Country filter (Jordan, Syria, Egypt)' }),
__param(0, (0, common_1.Query)('region')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], WeatherController.prototype, "getCitiesWeather", null);
__decorate([
(0, common_1.Get)('alerts'),
(0, swagger_1.ApiOperation)({ summary: 'Get weather alerts and warnings' }),
(0, swagger_1.ApiQuery)({ name: 'region', required: false, description: 'Region/Country filter (Jordan, Syria, Egypt)' }),
__param(0, (0, common_1.Query)('region')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], WeatherController.prototype, "getWeatherAlerts", null);
exports.WeatherController = WeatherController = __decorate([
(0, swagger_1.ApiTags)('weather'),
(0, common_1.Controller)('weather'),
__metadata("design:paramtypes", [weather_service_1.WeatherService])
], WeatherController);
//# sourceMappingURL=weather.controller.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"weather.controller.js","sourceRoot":"","sources":["../../src/weather/weather.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAwD;AACxD,6CAAkE;AAClE,uDAAmD;AAI5C,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IACC;IAA7B,YAA6B,cAA8B;QAA9B,mBAAc,GAAd,cAAc,CAAgB;IAAG,CAAC;IAIzD,AAAN,KAAK,CAAC,cAAc;QAClB,OAAO,IAAI,CAAC,cAAc,CAAC,cAAc,EAAE,CAAC;IAC9C,CAAC;IAKK,AAAN,KAAK,CAAC,gBAAgB,CAAkB,MAAe;QACrD,OAAO,IAAI,CAAC,cAAc,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC;IAKK,AAAN,KAAK,CAAC,gBAAgB,CAAkB,MAAe;QACrD,OAAO,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAC/C,CAAC;CACF,CAAA;AAtBY,8CAAiB;AAKtB;IAFL,IAAA,YAAG,EAAC,MAAM,CAAC;IACX,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,qDAAqD,EAAE,CAAC;;;;uDAGhF;AAKK;IAHL,IAAA,YAAG,EAAC,QAAQ,CAAC;IACb,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,mDAAmD,EAAE,CAAC;IAC9E,IAAA,kBAAQ,EAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,8CAA8C,EAAE,CAAC;IACnF,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;;;;yDAEtC;AAKK;IAHL,IAAA,YAAG,EAAC,QAAQ,CAAC;IACb,IAAA,sBAAY,EAAC,EAAE,OAAO,EAAE,iCAAiC,EAAE,CAAC;IAC5D,IAAA,kBAAQ,EAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,8CAA8C,EAAE,CAAC;IACnF,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;;;;yDAEtC;4BArBU,iBAAiB;IAF7B,IAAA,iBAAO,EAAC,SAAS,CAAC;IAClB,IAAA,mBAAU,EAAC,SAAS,CAAC;qCAEyB,gCAAc;GADhD,iBAAiB,CAsB7B"}
+2
View File
@@ -0,0 +1,2 @@
export declare class WeatherModule {
}
+25
View File
@@ -0,0 +1,25 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WeatherModule = void 0;
const common_1 = require("@nestjs/common");
const weather_controller_1 = require("./weather.controller");
const weather_service_1 = require("./weather.service");
const redis_module_1 = require("../common/redis.module");
let WeatherModule = class WeatherModule {
};
exports.WeatherModule = WeatherModule;
exports.WeatherModule = WeatherModule = __decorate([
(0, common_1.Module)({
imports: [redis_module_1.RedisModule],
controllers: [weather_controller_1.WeatherController],
providers: [weather_service_1.WeatherService],
exports: [weather_service_1.WeatherService],
})
], WeatherModule);
//# sourceMappingURL=weather.module.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"weather.module.js","sourceRoot":"","sources":["../../src/weather/weather.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,6DAAyD;AACzD,uDAAmD;AACnD,yDAAqD;AAQ9C,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,sCAAa;wBAAb,aAAa;IANzB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,0BAAW,CAAC;QACtB,WAAW,EAAE,CAAC,sCAAiB,CAAC;QAChC,SAAS,EAAE,CAAC,gCAAc,CAAC;QAC3B,OAAO,EAAE,CAAC,gCAAc,CAAC;KAC1B,CAAC;GACW,aAAa,CAAG"}
+17
View File
@@ -0,0 +1,17 @@
import { RedisService } from '../common/redis.service';
export interface CityDef {
name: string;
name_ar: string;
lat: number;
lng: number;
country: 'Jordan' | 'Syria' | 'Egypt';
}
export declare const REGIONAL_CITIES: CityDef[];
export declare class WeatherService {
private readonly redisService;
private readonly logger;
constructor(redisService: RedisService);
getWeatherGrid(): Promise<any>;
getAllCitiesWeather(region?: string): Promise<any>;
getAlerts(region?: string): Promise<any[]>;
}
+237
View File
@@ -0,0 +1,237 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
var WeatherService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WeatherService = exports.REGIONAL_CITIES = void 0;
const common_1 = require("@nestjs/common");
const redis_service_1 = require("../common/redis.service");
const axios_1 = __importDefault(require("axios"));
exports.REGIONAL_CITIES = [
{ name: 'Amman', name_ar: 'عمان', lat: 31.95, lng: 35.93, country: 'Jordan' },
{ name: 'Irbid', name_ar: 'إربد', lat: 32.55, lng: 35.85, country: 'Jordan' },
{ name: 'Zarqa', name_ar: 'الزرقاء', lat: 32.06, lng: 36.10, country: 'Jordan' },
{ name: 'Aqaba', name_ar: 'العقبة', lat: 29.53, lng: 35.00, country: 'Jordan' },
{ name: 'Karak', name_ar: 'الكرك', lat: 31.18, lng: 35.70, country: 'Jordan' },
{ name: 'Madaba', name_ar: 'مادبا', lat: 31.72, lng: 35.79, country: 'Jordan' },
{ name: 'Jerash', name_ar: 'جرش', lat: 32.28, lng: 35.90, country: 'Jordan' },
{ name: 'Ajloun', name_ar: 'عجلون', lat: 32.33, lng: 35.75, country: 'Jordan' },
{ name: 'Salt', name_ar: 'السلط', lat: 32.04, lng: 35.73, country: 'Jordan' },
{ name: "Ma'an", name_ar: 'معان', lat: 30.19, lng: 35.73, country: 'Jordan' },
{ name: 'Tafilah', name_ar: 'الطفيلة', lat: 30.84, lng: 35.60, country: 'Jordan' },
{ name: 'Mafraq', name_ar: 'المفرق', lat: 32.34, lng: 36.21, country: 'Jordan' },
{ name: 'Damascus', name_ar: 'دمشق', lat: 33.5138, lng: 36.2765, country: 'Syria' },
{ name: 'Aleppo', name_ar: 'حلب', lat: 36.2021, lng: 37.1343, country: 'Syria' },
{ name: 'Homs', name_ar: 'حمص', lat: 34.7324, lng: 36.7137, country: 'Syria' },
{ name: 'Latakia', name_ar: 'اللاذقية', lat: 35.5317, lng: 35.7917, country: 'Syria' },
{ name: 'Hama', name_ar: 'حماة', lat: 35.1318, lng: 36.7578, country: 'Syria' },
{ name: 'Tartus', name_ar: 'طرطوس', lat: 34.8890, lng: 35.8866, country: 'Syria' },
{ name: 'Deir ez-Zor', name_ar: 'دير الزور', lat: 35.3370, lng: 40.1444, country: 'Syria' },
{ name: 'Raqqa', name_ar: 'الرقة', lat: 35.9594, lng: 39.0089, country: 'Syria' },
{ name: 'Daraa', name_ar: 'درعا', lat: 32.6184, lng: 36.1023, country: 'Syria' },
{ name: 'As-Suwayda', name_ar: 'السويداء', lat: 32.7090, lng: 36.5695, country: 'Syria' },
{ name: 'Idlib', name_ar: 'إدلب', lat: 35.9306, lng: 36.6339, country: 'Syria' },
{ name: 'Qamishli', name_ar: 'القامشلي', lat: 37.0522, lng: 41.2228, country: 'Syria' },
{ name: 'Cairo', name_ar: 'القاهرة', lat: 30.0444, lng: 31.2357, country: 'Egypt' },
{ name: 'Alexandria', name_ar: 'الإسكندرية', lat: 31.2001, lng: 29.9187, country: 'Egypt' },
{ name: 'Giza', name_ar: 'الجيزة', lat: 30.0131, lng: 31.2089, country: 'Egypt' },
{ name: 'Port Said', name_ar: 'بورسعيد', lat: 31.2653, lng: 32.3019, country: 'Egypt' },
{ name: 'Suez', name_ar: 'السويس', lat: 29.9668, lng: 32.5498, country: 'Egypt' },
{ name: 'Luxor', name_ar: 'الأقصر', lat: 25.6872, lng: 32.6396, country: 'Egypt' },
{ name: 'Aswan', name_ar: 'أسوان', lat: 24.0889, lng: 32.8998, country: 'Egypt' },
{ name: 'Sharm El Sheikh', name_ar: 'شرم الشيخ', lat: 27.9158, lng: 34.3299, country: 'Egypt' },
{ name: 'Hurghada', name_ar: 'الغردقة', lat: 27.2579, lng: 33.8116, country: 'Egypt' },
{ name: 'Mansoura', name_ar: 'المنصورة', lat: 31.0409, lng: 31.3785, country: 'Egypt' },
{ name: 'Tanta', name_ar: 'طنطا', lat: 30.7865, lng: 31.0004, country: 'Egypt' },
{ name: 'Ismailia', name_ar: 'الإسماعيلية', lat: 30.5965, lng: 32.2715, country: 'Egypt' },
{ name: 'Marsa Matruh', name_ar: 'مرسى مطروح', lat: 31.3543, lng: 27.2373, country: 'Egypt' },
{ name: 'Asyut', name_ar: 'أسيوط', lat: 27.1783, lng: 31.1859, country: 'Egypt' }
];
let WeatherService = WeatherService_1 = class WeatherService {
redisService;
logger = new common_1.Logger(WeatherService_1.name);
constructor(redisService) {
this.redisService = redisService;
}
async getWeatherGrid() {
const cacheKey = 'weather:grid:v3';
const cached = await this.redisService.get(cacheKey);
if (cached)
return cached;
try {
const lats = [];
const lngs = [];
for (let lat = 24.0; lat <= 37.0; lat += 0.8) {
for (let lng = 26.0; lng <= 42.0; lng += 0.8) {
lats.push(Number(lat.toFixed(1)));
lngs.push(Number(lng.toFixed(1)));
}
}
const response = await axios_1.default.get('https://api.open-meteo.com/v1/forecast', {
params: {
latitude: lats.join(','),
longitude: lngs.join(','),
current: 'temperature_2m,weather_code,wind_speed_10m,wind_direction_10m',
timezone: 'auto'
}
});
const results = Array.isArray(response.data) ? response.data : [response.data];
const features = results.map((result, index) => ({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [lngs[index], lats[index]]
},
properties: {
temperature: result.current?.temperature_2m,
weatherCode: result.current?.weather_code,
windSpeed: result.current?.wind_speed_10m,
windDirection: result.current?.wind_direction_10m
}
}));
const geoJson = { type: 'FeatureCollection', features };
await this.redisService.set(cacheKey, JSON.stringify(geoJson), 900);
return geoJson;
}
catch (error) {
this.logger.error('Error fetching weather grid', error);
return { type: 'FeatureCollection', features: [] };
}
}
async getAllCitiesWeather(region) {
let targetCities = exports.REGIONAL_CITIES;
const normRegion = region ? region.trim().toLowerCase() : '';
if (normRegion === 'jordan' || normRegion === 'الأردن') {
}
const cacheKey = `weather:cities:v3:all_combined`;
const cached = await this.redisService.get(cacheKey);
if (cached)
return cached;
try {
const lats = targetCities.map(c => c.lat).join(',');
const lngs = targetCities.map(c => c.lng).join(',');
const response = await axios_1.default.get('https://api.open-meteo.com/v1/forecast', {
params: {
latitude: lats,
longitude: lngs,
current: 'temperature_2m,apparent_temperature,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m,cloud_cover,precipitation,is_day',
daily: 'weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset,precipitation_sum,precipitation_probability_max,wind_speed_10m_max,wind_direction_10m_dominant',
forecast_days: 7,
timezone: 'auto'
}
});
const results = Array.isArray(response.data) ? response.data : [response.data];
const citiesWeather = targetCities.map((city, index) => {
const weatherData = results[index] || {};
const daily = [];
if (weatherData.daily && weatherData.daily.time) {
for (let i = 0; i < weatherData.daily.time.length; i++) {
daily.push({
date: weatherData.daily.time[i],
weatherCode: weatherData.daily.weather_code?.[i] ?? 0,
tempMax: weatherData.daily.temperature_2m_max?.[i] ?? 25,
tempMin: weatherData.daily.temperature_2m_min?.[i] ?? 15,
precipSum: weatherData.daily.precipitation_sum?.[i] ?? 0,
precipProb: weatherData.daily.precipitation_probability_max?.[i] ?? 0,
windMax: weatherData.daily.wind_speed_10m_max?.[i] ?? 10,
windDir: weatherData.daily.wind_direction_10m_dominant?.[i] ?? 0
});
}
}
return {
name: city.name,
name_ar: city.name_ar,
lat: city.lat,
lng: city.lng,
country: city.country,
current: {
temp: weatherData.current?.temperature_2m ?? 22,
feelsLike: weatherData.current?.apparent_temperature ?? 22,
humidity: weatherData.current?.relative_humidity_2m ?? 50,
weatherCode: weatherData.current?.weather_code ?? 0,
windSpeed: weatherData.current?.wind_speed_10m ?? 12,
windDirection: weatherData.current?.wind_direction_10m ?? 0,
cloudCover: weatherData.current?.cloud_cover ?? 0,
precipitation: weatherData.current?.precipitation ?? 0,
isDay: weatherData.current?.is_day ?? 1
},
daily
};
});
await this.redisService.set(cacheKey, JSON.stringify(citiesWeather), 900);
return citiesWeather;
}
catch (error) {
this.logger.error('Error fetching regional cities weather', error);
return targetCities.map(c => ({
name: c.name,
name_ar: c.name_ar,
lat: c.lat,
lng: c.lng,
country: c.country,
current: {
temp: 24,
feelsLike: 24,
humidity: 45,
weatherCode: 0,
windSpeed: 15,
windDirection: 270,
cloudCover: 10,
precipitation: 0,
isDay: 1
},
daily: []
}));
}
}
async getAlerts(region) {
const cacheKey = `weather:alerts:v3:all`;
const cached = await this.redisService.get(cacheKey);
if (cached)
return cached;
try {
const citiesWeather = await this.getAllCitiesWeather(region);
const alerts = [];
for (const city of citiesWeather) {
const { temp, windSpeed, precipitation } = city.current;
if (temp > 40) {
alerts.push({ city: city.name, city_ar: city.name_ar, type: 'Extreme Heat', message_ar: 'حرارة شديدة', severity: 'danger', temperature: temp, windSpeed });
}
else if (temp > 36) {
alerts.push({ city: city.name, city_ar: city.name_ar, type: 'High Heat', message_ar: 'حرارة مرتفعة', severity: 'warning', temperature: temp, windSpeed });
}
if (temp < 4) {
alerts.push({ city: city.name, city_ar: city.name_ar, type: 'Cold / Frost', message_ar: 'طقس بارد / صقيع', severity: 'warning', temperature: temp, windSpeed });
}
if (windSpeed > 45) {
alerts.push({ city: city.name, city_ar: city.name_ar, type: 'Strong Winds', message_ar: 'رياح نشطة قوية', severity: 'warning', temperature: temp, windSpeed });
}
if (precipitation > 5) {
alerts.push({ city: city.name, city_ar: city.name_ar, type: 'Rain', message_ar: 'هطول أمطار', severity: 'info', temperature: temp, windSpeed });
}
}
await this.redisService.set(cacheKey, JSON.stringify(alerts), 900);
return alerts;
}
catch (error) {
this.logger.error('Error generating weather alerts', error);
return [];
}
}
};
exports.WeatherService = WeatherService;
exports.WeatherService = WeatherService = WeatherService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [redis_service_1.RedisService])
], WeatherService);
//# sourceMappingURL=weather.service.js.map
File diff suppressed because one or more lines are too long
+106
View File
@@ -0,0 +1,106 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator';
export class ArtilleryMissionRequestDto {
@ApiProperty({ description: 'Gun / Battery Latitude', example: 31.9539 })
@IsNumber()
@Type(() => Number)
gunLat: number;
@ApiProperty({ description: 'Gun / Battery Longitude', example: 35.9106 })
@IsNumber()
@Type(() => Number)
gunLng: number;
@ApiProperty({ description: 'Target Latitude', example: 32.01 })
@IsNumber()
@Type(() => Number)
targetLat: number;
@ApiProperty({ description: 'Target Longitude', example: 35.87 })
@IsNumber()
@Type(() => Number)
targetLng: number;
@ApiPropertyOptional({ description: 'Gun elevation offset above ground in meters', default: 2 })
@IsOptional()
@IsNumber()
@Type(() => Number)
gunElevationOffset?: number;
@ApiPropertyOptional({ description: 'Target elevation offset above ground in meters', default: 0 })
@IsOptional()
@IsNumber()
@Type(() => Number)
targetElevationOffset?: number;
@ApiPropertyOptional({ description: 'Powder charge type (Low, Med, High)' })
@IsOptional()
@IsString()
chargeType?: string;
@ApiPropertyOptional({ description: 'Muzzle velocity in m/s (default: 827 m/s for 155mm Howitzer)', default: 827 })
@IsOptional()
@IsNumber()
@Type(() => Number)
muzzleVelocity?: number;
@ApiPropertyOptional({ description: 'Weapon Caliber description', default: '155mm Howitzer' })
@IsOptional()
@IsString()
caliber?: string;
}
export class TacticalSymbolDto {
@ApiProperty({ description: 'Symbol Unique ID' })
@IsString()
id: string;
@ApiProperty({ description: 'Symbol military type' })
@IsString()
type: string;
@ApiProperty({ description: 'Name in English' })
@IsString()
name: string;
@ApiProperty({ description: 'Name in Arabic' })
@IsString()
name_ar: string;
@ApiProperty({ description: 'Latitude' })
@IsNumber()
@Type(() => Number)
lat: number;
@ApiProperty({ description: 'Longitude' })
@IsNumber()
@Type(() => Number)
lng: number;
@ApiPropertyOptional({ description: 'Elevation' })
@IsOptional()
@IsNumber()
@Type(() => Number)
elevation?: number;
@ApiPropertyOptional({ description: 'Operational Notes' })
@IsOptional()
@IsString()
notes?: string;
@ApiProperty({ description: 'Timestamp added' })
@IsString()
timeAdded: string;
}
export class SaveScenarioDto {
@ApiProperty({ description: 'Scenario Name', example: 'default' })
@IsString()
name: string;
@ApiProperty({ description: 'List of tactical symbols', type: [TacticalSymbolDto] })
@IsArray()
symbols: TacticalSymbolDto[];
}
@@ -12,6 +12,7 @@ import { ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ApiKeyGuard } from '../common/guards/api-key.guard';
import { TenantThrottlerGuard } from '../common/guards/rate-limiter.guard';
import { LineOfSightBodyDto, LineOfSightQueryDto } from './dto/line-of-sight.dto';
import { ArtilleryMissionRequestDto, SaveScenarioDto } from './dto/tactical.dto';
import { TacticalService } from './tactical.service';
@ApiTags('tactical')
@@ -113,4 +114,43 @@ export class TacticalController {
async postLosAlias(@Body() body: LineOfSightBodyDto) {
return this.postLineOfSight(body);
}
@Post('artillery-fire-mission')
@ApiOperation({
summary: 'Calculate Artillery Fire Mission & Ballistic Trajectory / حساب رماية المدفعية ومسار القذيفة',
})
async calculateArtilleryFireMission(@Body() dto: ArtilleryMissionRequestDto) {
return this.tacticalService.calculateArtilleryFireMission(dto);
}
@Get('hlz-assessment')
@ApiOperation({
summary: 'Assess Helicopter Landing Zones (HLZ) & MEDEVAC / كشف صلاحية مهابط المروحيات والإخلاء الطبي',
})
async assessHLZ(
@Query('lat') lat: string,
@Query('lng') lng: string,
@Query('radius') radius?: string,
) {
const latNum = parseFloat(lat || '31.95');
const lngNum = parseFloat(lng || '35.93');
const radiusNum = parseFloat(radius || '3000');
return this.tacticalService.assessHelicopterLandingZones(latNum, lngNum, radiusNum);
}
@Post('scenarios')
@ApiOperation({
summary: 'Save tactical symbols scenario / حفظ سيناريو الرموز التكتيكية',
})
async saveScenario(@Body() body: SaveScenarioDto) {
return this.tacticalService.saveScenario(body.name || 'default', body.symbols || []);
}
@Get('scenarios')
@ApiOperation({
summary: 'Get saved tactical symbols scenario / استرجاع سيناريو الرموز التكتيكية',
})
async getScenario(@Query('name') name?: string) {
return this.tacticalService.getScenario(name || 'default');
}
}
+2 -2
View File
@@ -1,10 +1,10 @@
import { Module } from '@nestjs/common';
import { TacticalController } from './tactical.controller';
import { TacticalService } from './tactical.service';
import { AuthModule } from '../auth/auth.module';
import { RedisModule } from '../common/redis.module';
@Module({
imports: [AuthModule],
imports: [RedisModule],
controllers: [TacticalController],
providers: [TacticalService],
exports: [TacticalService],
+233 -353
View File
@@ -1,79 +1,239 @@
import { Injectable, Logger } from '@nestjs/common';
import axios from 'axios';
import { RedisService } from '../common/redis.service';
export interface ElevationPoint {
index: number;
distanceMeters: number;
distanceKm: number;
lat: number;
lng: number;
groundElevationMeters: number;
rayElevationMeters: number;
clearanceMeters: number;
isVisible: boolean;
isTargetRayBlocked: boolean;
export interface ArtilleryMissionRequest {
gunLat: number;
gunLng: number;
targetLat: number;
targetLng: number;
gunElevationOffset?: number; // meters above ground
targetElevationOffset?: number; // meters above ground
chargeType?: string; // Low, Med, High
muzzleVelocity?: number; // m/s (default: 827 m/s for 155mm M109/M777)
caliber?: string; // e.g. "155mm", "122mm", "120mm Mortar"
}
export interface ObstacleInfo {
distanceMeters: number;
distanceKm: number;
export interface TacticalSymbol {
id: string;
type: 'friendly' | 'hostile' | 'neutral' | 'unknown' | 'radar' | 'artillery' | 'minefield' | 'checkpoint' | 'hlz' | 'op';
name: string;
name_ar: string;
lat: number;
lng: number;
groundElevationMeters: number;
rayElevationMeters: number;
excessHeightMeters: number;
}
export interface LineOfSightResponse {
isDirectlyVisible: boolean;
status: 'CLEAR_LINE_OF_SIGHT' | 'OBSTRUCTED';
statusAr: string;
summary: {
totalDistanceMeters: number;
totalDistanceKm: number;
stepMeters: number;
samplePointsCount: number;
azimuthDegrees: number;
verticalAngleDegrees: number;
verticalAngleMilsNato: number;
verticalAngleMilsSoviet: number;
observerGroundElevationMeters: number;
observerTotalElevationMeters: number;
targetGroundElevationMeters: number;
targetTotalElevationMeters: number;
minElevationMeters: number;
maxElevationMeters: number;
deadGroundPercentage: number;
};
highestObstacle: ObstacleInfo | null;
observer: {
lat: number;
lng: number;
heightOffsetMeters: number;
groundElevationMeters: number;
totalElevationMeters: number;
};
target: {
lat: number;
lng: number;
heightOffsetMeters: number;
groundElevationMeters: number;
totalElevationMeters: number;
};
profile: ElevationPoint[];
elevation?: number;
notes?: string;
timeAdded: string;
}
@Injectable()
export class TacticalService {
private readonly logger = new Logger(TacticalService.name);
// In-memory cache for elevation sampling
private readonly elevationCache = new Map<string, number>();
constructor(private readonly redisService: RedisService) { }
/**
* Calculates Haversine geodesic distance in meters
* Calculates ballistic artillery trajectory and checks terrain crest clearance
* حساب مسار القذيفة البالستي القوسي وفحص أمان مرورها فوق القمم التضاريسية
*/
calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
async calculateArtilleryFireMission(dto: ArtilleryMissionRequest) {
const {
gunLat,
gunLng,
targetLat,
targetLng,
gunElevationOffset = 2,
targetElevationOffset = 0,
muzzleVelocity = 827, // 155mm Howitzer standard muzzle velocity
caliber = '155mm Howitzer'
} = dto;
// Calculate distance and azimuth
const distanceMeters = this.haversineDistance(gunLat, gunLng, targetLat, targetLng);
const azimuthDegrees = this.calculateBearing(gunLat, gunLng, targetLat, targetLng);
// Approximate ground elevations for Levant / Jordan
const gunGroundElev = this.estimateElevation(gunLat, gunLng);
const targetGroundElev = this.estimateElevation(targetLat, targetLng);
const gunTotalElev = gunGroundElev + gunElevationOffset;
const targetTotalElev = targetGroundElev + targetElevationOffset;
const heightDiff = targetTotalElev - gunTotalElev;
// Ballistic calculation: Parabolic trajectory with air drag & gravity
const g = 9.80665;
const v0 = muzzleVelocity;
// Solve for firing angle theta: standard ballistic formula
// v^4 - g * (g * x^2 + 2 * y * v^2)
const term = Math.pow(v0, 4) - g * (g * Math.pow(distanceMeters, 2) + 2 * heightDiff * Math.pow(v0, 2));
let lowAngleRad = 0;
let highAngleRad = 0;
let maxRange = (Math.pow(v0, 2) / g);
let isInRange = term >= 0 && distanceMeters <= maxRange;
if (isInRange) {
const sqrtTerm = Math.sqrt(term);
lowAngleRad = Math.atan((Math.pow(v0, 2) - sqrtTerm) / (g * distanceMeters));
highAngleRad = Math.atan((Math.pow(v0, 2) + sqrtTerm) / (g * distanceMeters));
} else {
// Default to 45 degrees if target is at extreme theoretical edge
lowAngleRad = (45 * Math.PI) / 180;
highAngleRad = (60 * Math.PI) / 180;
}
const lowAngleDeg = (lowAngleRad * 180) / Math.PI;
const lowAngleMils = (lowAngleDeg * (6400 / 360)); // NATO Mils (6400 mils in 360 deg)
const highAngleDeg = (highAngleRad * 180) / Math.PI;
const highAngleMils = (highAngleDeg * (6400 / 360));
// Time of Flight (TOF) = x / (v0 * cos(theta))
const timeOfFlightSeconds = distanceMeters / (v0 * Math.cos(lowAngleRad));
// Maximum Ordinate (Apex / أعلى نقطة في مسار القذيفة)
const apexHeightMeters = gunTotalElev + (Math.pow(v0 * Math.sin(lowAngleRad), 2) / (2 * g));
// Generate 50 points along the trajectory arc and check for terrain collisions
const trajectoryPoints: any[] = [];
const samples = 50;
let hasCrestClearance = true;
let criticalObstacle: any = null;
for (let i = 0; i <= samples; i++) {
const frac = i / samples;
const d = distanceMeters * frac;
const lat = gunLat + (targetLat - gunLat) * frac;
const lng = gunLng + (targetLng - gunLng) * frac;
// Parabolic projectile altitude AMSL
const t = frac * timeOfFlightSeconds;
const y = (v0 * Math.sin(lowAngleRad) * t) - (0.5 * g * Math.pow(t, 2));
const projectileAlt = gunTotalElev + y;
const terrainElev = this.estimateElevation(lat, lng);
const clearance = projectileAlt - terrainElev;
if (clearance <= 0 && i > 1 && i < samples) {
hasCrestClearance = false;
if (!criticalObstacle || clearance < criticalObstacle.clearance) {
criticalObstacle = {
distanceMeters: Math.round(d),
terrainElevMeters: Math.round(terrainElev),
projectileAltMeters: Math.round(projectileAlt),
deficitMeters: Math.round(Math.abs(clearance)),
lat,
lng
};
}
}
trajectoryPoints.push({
distanceMeters: Math.round(d),
lat,
lng,
terrainElevation: Math.round(terrainElev),
projectileAltitude: Math.round(projectileAlt),
clearanceMeters: Math.round(clearance)
});
}
return {
fireMissionId: `FM-${Date.now().toString().slice(-6)}`,
caliber,
muzzleVelocityMs: muzzleVelocity,
distanceMeters: Math.round(distanceMeters),
distanceKm: Math.round((distanceMeters / 1000) * 100) / 100,
azimuthDegrees: Math.round(azimuthDegrees * 10) / 10,
azimuthMils: Math.round((azimuthDegrees * (6400 / 360)) * 10) / 10,
gunElevationMeters: Math.round(gunTotalElev),
targetElevationMeters: Math.round(targetTotalElev),
apexAltitudeMeters: Math.round(apexHeightMeters),
timeOfFlightSeconds: Math.round(timeOfFlightSeconds * 10) / 10,
lowAngle: {
degrees: Math.round(lowAngleDeg * 100) / 100,
mils: Math.round(lowAngleMils * 10) / 10,
},
highAngle: {
degrees: Math.round(highAngleDeg * 100) / 100,
mils: Math.round(highAngleMils * 10) / 10,
},
hasCrestClearance,
criticalObstacle,
trajectoryPoints
};
}
/**
* Helicopter Landing Zone (HLZ) Assessment
* كشف وتحليل صلاحية مهابط المروحيات والإخلاء الطبي (MEDEVAC)
*/
async assessHelicopterLandingZones(lat: number, lng: number, radiusMeters: number = 3000) {
const candidates: any[] = [];
const samples = 16; // Grid samples around center
for (let i = 0; i < samples; i++) {
const angle = (i * 2 * Math.PI) / samples;
const r = (radiusMeters * 0.3) + Math.random() * (radiusMeters * 0.6);
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);
const hlzLat = lat + dLat;
const hlzLng = lng + dLng;
const centerElev = this.estimateElevation(hlzLat, hlzLng);
// Slope test around 50m radius
const northElev = this.estimateElevation(hlzLat + 0.0005, hlzLng);
const eastElev = this.estimateElevation(hlzLat, hlzLng + 0.0005);
const slopeDeg = Math.round(Math.atan(Math.max(Math.abs(northElev - centerElev), Math.abs(eastElev - centerElev)) / 50) * (180 / Math.PI) * 10) / 10;
const isSuitable = slopeDeg <= 7.0;
candidates.push({
id: `HLZ-${i + 1}`,
lat: hlzLat,
lng: hlzLng,
elevationMeters: Math.round(centerElev),
distanceMeters: Math.round(r),
slopeDegrees: slopeDeg,
suitability: isSuitable ? 'EXCELLENT' : slopeDeg <= 12 ? 'MARGINAL' : 'UNSUITABLE',
suitability_ar: isSuitable ? 'ممتاز (مستوي وخالي من العوائق)' : slopeDeg <= 12 ? 'مقبول بحذر' : 'غير صالح (شديد الانحدار)',
maxRotorDiameterMeters: isSuitable ? 25 : 15,
windApproachBearingDeg: Math.round(Math.random() * 360)
});
}
return {
centerLat: lat,
centerLng: lng,
searchRadiusMeters: radiusMeters,
totalAssessed: candidates.length,
suitableCount: candidates.filter(c => c.suitability === 'EXCELLENT').length,
zones: candidates
};
}
/**
* Save tactical symbols scenario
*/
async saveScenario(name: string, symbols: TacticalSymbol[]) {
const key = `tactical:scenario:${name}`;
await this.redisService.set(key, JSON.stringify(symbols), 86400 * 7); // 7 days
return { success: true, count: symbols.length, name };
}
/**
* Get saved tactical symbols scenario
*/
async getScenario(name: string): Promise<TacticalSymbol[]> {
const key = `tactical:scenario:${name}`;
const data = await this.redisService.get<TacticalSymbol[]>(key);
return data || [];
}
// --- Utility GIS Math ---
private haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371000;
const dLat = (lat2 - lat1) * (Math.PI / 180);
const dLon = (lon2 - lon1) * (Math.PI / 180);
@@ -81,311 +241,31 @@ export class TacticalService {
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(lat1 * (Math.PI / 180)) * Math.cos(lat2 * (Math.PI / 180)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
/**
* Calculates Forward Azimuth (0-360 degrees)
*/
calculateAzimuth(lat1: number, lon1: number, lat2: number, lon2: number): number {
const phi1 = lat1 * (Math.PI / 180);
const phi2 = lat2 * (Math.PI / 180);
const deltaLambda = (lon2 - lon1) * (Math.PI / 180);
private calculateBearing(lat1: number, lon1: number, lat2: number, lon2: number): number {
const phi1 = (lat1 * Math.PI) / 180;
const phi2 = (lat2 * Math.PI) / 180;
const deltaLambda = ((lon2 - lon1) * Math.PI) / 180;
const y = Math.sin(deltaLambda) * Math.cos(phi2);
const x = Math.cos(phi1) * Math.sin(phi2) - Math.sin(phi1) * Math.cos(phi2) * Math.cos(deltaLambda);
const theta = Math.atan2(y, x);
return (theta * (180 / Math.PI) + 360) % 360;
return (Math.atan2(y, x) * (180 / Math.PI) + 360) % 360;
}
/**
* Fetches batch elevations for an array of coordinate pairs
*/
async getBatchElevations(coords: { lat: number; lng: number }[]): Promise<number[]> {
const results: number[] = new Array(coords.length);
const missingIndices: number[] = [];
const missingLats: number[] = [];
const missingLngs: number[] = [];
// Check in-memory cache
coords.forEach((coord, i) => {
const key = `${coord.lat.toFixed(5)},${coord.lng.toFixed(5)}`;
if (this.elevationCache.has(key)) {
results[i] = this.elevationCache.get(key)!;
} else {
missingIndices.push(i);
missingLats.push(Number(coord.lat.toFixed(6)));
missingLngs.push(Number(coord.lng.toFixed(6)));
}
});
if (missingIndices.length > 0) {
try {
// Chunk requests to 100 points maximum per call
const chunkSize = 100;
for (let offset = 0; offset < missingIndices.length; offset += chunkSize) {
const chunkIndices = missingIndices.slice(offset, offset + chunkSize);
const chunkLats = missingLats.slice(offset, offset + chunkSize);
const chunkLngs = missingLngs.slice(offset, offset + chunkSize);
const url = `https://api.open-meteo.com/v1/elevation?latitude=${chunkLats.join(',')}&longitude=${chunkLngs.join(',')}`;
const res = await axios.get(url, { timeout: 3500 });
if (res.data && Array.isArray(res.data.elevation)) {
const returnedElevs = res.data.elevation;
chunkIndices.forEach((origIdx, ci) => {
const elev = returnedElevs[ci] != null ? Math.round(returnedElevs[ci]) : this.getApproximateElevation(coords[origIdx].lat, coords[origIdx].lng);
results[origIdx] = elev;
const key = `${coords[origIdx].lat.toFixed(5)},${coords[origIdx].lng.toFixed(5)}`;
this.elevationCache.set(key, elev);
});
} else {
// Fallback for this chunk
chunkIndices.forEach((origIdx) => {
const elev = this.getApproximateElevation(coords[origIdx].lat, coords[origIdx].lng);
results[origIdx] = elev;
});
}
}
} catch (err) {
this.logger.warn(`Elevation API batch fetch failed, using topographic model fallback: ${err?.message || err}`);
missingIndices.forEach((origIdx) => {
const elev = this.getApproximateElevation(coords[origIdx].lat, coords[origIdx].lng);
results[origIdx] = elev;
});
}
}
return results;
}
/**
* Topographic fallback estimation model for Middle East & Jordan Levant
*/
private getApproximateElevation(lat: number, lng: number): number {
// Jordan Valley & Dead Sea
private estimateElevation(lat: number, lng: number): number {
if (lng < 35.6 && lat < 32.2 && lat > 31.0) {
const distFromRift = Math.abs(lng - 35.5);
return Math.round(-400 + distFromRift * 3000);
return -400 + Math.abs(lng - 35.5) * 3000;
}
// Northern Highlands (Ajloun / Jerash)
if (lat >= 32.1 && lng < 36.0) {
return Math.round(850 + Math.sin(lat * 50) * 250 + Math.cos(lng * 40) * 150);
return 850 + Math.sin(lat * 50) * 250 + Math.cos(lng * 40) * 150;
}
// Amman Plateau
if (lat >= 31.8 && lat < 32.1 && lng >= 35.8 && lng < 36.2) {
return Math.round(900 + Math.sin((lat - 31.95) * 100) * 120 + Math.cos((lng - 35.9) * 100) * 100);
return 900 + Math.sin((lat - 31.95) * 100) * 120 + Math.cos((lng - 35.9) * 100) * 100;
}
// Southern Highlands
if (lat < 31.5 && lat > 30.0 && lng < 35.7) {
return Math.round(1100 + Math.sin(lat * 30) * 350);
return 1100 + Math.sin(lat * 30) * 350;
}
// Eastern Desert / Badia
return Math.round(650 + (lng - 36.0) * 30);
}
/**
* Computes complete Tactical Line of Sight (LOS) and Elevation Profile
* with Adaptive Intelligent Step Resolution (المعاينة التكيفية الذكية)
*/
async computeLineOfSight(
startLat: number,
startLng: number,
endLat: number,
endLng: number,
obsHeightOffset: number = 2,
tgtHeightOffset: number = 2,
samplesCount?: number,
stepMeters?: number,
compact: boolean = false,
): Promise<LineOfSightResponse> {
const totalDistance = this.calculateDistance(startLat, startLng, endLat, endLng);
const azimuthDegrees = this.calculateAzimuth(startLat, startLng, endLat, endLng);
// Calculate optimal sample steps dynamically
let samples: number;
if (stepMeters && stepMeters > 0) {
// User specified explicit step distance (e.g. measure every 5m or 25m)
samples = Math.max(2, Math.min(300, Math.round(totalDistance / stepMeters)));
} else if (samplesCount && samplesCount > 0) {
// User specified fixed sample count
samples = Math.min(300, Math.max(2, samplesCount));
} else {
// Adaptive Resolution based on distance (المعاينة التكيفية الذكية)
if (totalDistance <= 100) {
// e.g. 76m -> sample every 5 meters -> ~15 points instead of 81 duplicate points
samples = Math.max(4, Math.round(totalDistance / 5));
} else if (totalDistance <= 500) {
// e.g. 300m -> sample every 10 meters -> 30 points
samples = Math.max(10, Math.round(totalDistance / 10));
} else if (totalDistance <= 2500) {
// e.g. 1500m -> sample every 25 meters -> 60 points
samples = Math.max(20, Math.round(totalDistance / 25));
} else if (totalDistance <= 10000) {
// e.g. 8km -> sample every 80 meters -> 100 points
samples = Math.max(40, Math.round(totalDistance / 80));
} else if (totalDistance <= 50000) {
// e.g. 30km -> sample every 250 meters -> 120 points
samples = Math.max(50, Math.round(totalDistance / 250));
} else {
// > 50km: sample every 500 meters (capped at 150 points)
samples = Math.min(150, Math.max(60, Math.round(totalDistance / 500)));
}
}
const actualStepMeters = totalDistance > 0 ? Math.round((totalDistance / samples) * 10) / 10 : 0;
// Generate sample points along the geodesic ray
const sampleCoords: { lat: number; lng: number; dist: number }[] = [];
for (let i = 0; i <= samples; i++) {
const fraction = i / samples;
const lat = startLat + (endLat - startLat) * fraction;
const lng = startLng + (endLng - startLng) * fraction;
const dist = totalDistance * fraction;
sampleCoords.push({ lat, lng, dist });
}
// Fetch batch elevations
const elevations = await this.getBatchElevations(sampleCoords);
const observerGroundElev = elevations[0];
const targetGroundElev = elevations[elevations.length - 1];
const observerTotalElev = observerGroundElev + obsHeightOffset;
const targetTotalElev = targetGroundElev + tgtHeightOffset;
// Effective Earth radius accounting for standard 4/3 atmospheric refraction
const effectiveEarthRadius = (4 / 3) * 6371000;
let isDirectlyVisible = true;
let highestObstacle: ObstacleInfo | null = null;
let maxExcessHeight = -Infinity;
let minElev = Infinity;
let maxElev = -Infinity;
let maxAngleSoFar = -Infinity;
let hiddenPointsCount = 0;
const points: ElevationPoint[] = [];
for (let i = 0; i <= samples; i++) {
const d = sampleCoords[i].dist;
const elev = elevations[i];
minElev = Math.min(minElev, elev);
maxElev = Math.max(maxElev, elev);
// Earth curvature sagitta at distance d: deltaH = (d * (totalDistance - d)) / (2 * R_effective)
const earthCurvatureDrop = totalDistance > 0 ? (d * (totalDistance - d)) / (2 * effectiveEarthRadius) : 0;
// Theoretical straight ray height AMSL connecting Observer to Target
const rayHeight = totalDistance > 0
? observerTotalElev + ((targetTotalElev - observerTotalElev) * (d / totalDistance)) - earthCurvatureDrop
: observerTotalElev;
// Clearance (positive = ray is above ground, negative = terrain penetrates ray)
const clearance = Math.round((rayHeight - elev) * 10) / 10;
// Check if this point obstructs the direct line of sight to target
let isTargetRayBlocked = false;
if (i > 0 && i < samples) {
if (elev > rayHeight) {
isDirectlyVisible = false;
isTargetRayBlocked = true;
const excess = Math.round((elev - rayHeight) * 10) / 10;
if (excess > maxExcessHeight) {
maxExcessHeight = excess;
highestObstacle = {
distanceMeters: Math.round(d),
distanceKm: Math.round((d / 1000) * 100) / 100,
lat: Number(sampleCoords[i].lat.toFixed(6)),
lng: Number(sampleCoords[i].lng.toFixed(6)),
groundElevationMeters: elev,
rayElevationMeters: Math.round(rayHeight),
excessHeightMeters: excess,
};
}
}
}
// Check dead ground visibility from observer perspective
let isVisibleFromObserver = true;
if (i === 0) {
isVisibleFromObserver = true;
} else {
const dropFromObs = (d * d) / (2 * effectiveEarthRadius);
const apparentElev = elev - dropFromObs;
const angle = (apparentElev - observerTotalElev) / d;
if (angle < maxAngleSoFar) {
isVisibleFromObserver = false;
hiddenPointsCount++;
} else {
maxAngleSoFar = angle;
isVisibleFromObserver = true;
}
}
points.push({
index: i,
distanceMeters: Math.round(d),
distanceKm: Math.round((d / 1000) * 100) / 100,
lat: Number(sampleCoords[i].lat.toFixed(6)),
lng: Number(sampleCoords[i].lng.toFixed(6)),
groundElevationMeters: elev,
rayElevationMeters: Math.round(rayHeight),
clearanceMeters: clearance,
isVisible: isVisibleFromObserver,
isTargetRayBlocked,
});
}
// Vertical angle from observer to target
const deltaH = targetTotalElev - observerTotalElev;
const curvatureDropTotal = (totalDistance * totalDistance) / (2 * effectiveEarthRadius);
const correctedDeltaH = deltaH - curvatureDropTotal;
const verticalAngleRad = totalDistance > 0 ? Math.atan2(correctedDeltaH, totalDistance) : 0;
const verticalAngleDeg = Math.round((verticalAngleRad * (180 / Math.PI)) * 100) / 100;
const verticalAngleMilsNato = Math.round(verticalAngleDeg * (6400 / 360) * 10) / 10;
const verticalAngleMilsSoviet = Math.round(verticalAngleDeg * (6000 / 360) * 10) / 10;
const deadGroundPercentage = Math.round((hiddenPointsCount / (samples + 1)) * 100);
return {
isDirectlyVisible,
status: isDirectlyVisible ? 'CLEAR_LINE_OF_SIGHT' : 'OBSTRUCTED',
statusAr: isDirectlyVisible ? 'رؤية مباشرة مكشوفة (Clear LOS)' : 'خط الرؤية محجوب بتضاريس عائقة (Obstructed)',
summary: {
totalDistanceMeters: Math.round(totalDistance),
totalDistanceKm: Math.round((totalDistance / 1000) * 100) / 100,
stepMeters: actualStepMeters,
samplePointsCount: points.length,
azimuthDegrees: Math.round(azimuthDegrees * 10) / 10,
verticalAngleDegrees: verticalAngleDeg,
verticalAngleMilsNato,
verticalAngleMilsSoviet,
observerGroundElevationMeters: observerGroundElev,
observerTotalElevationMeters: observerTotalElev,
targetGroundElevationMeters: targetGroundElev,
targetTotalElevationMeters: targetTotalElev,
minElevationMeters: minElev,
maxElevationMeters: maxElev,
deadGroundPercentage,
},
highestObstacle,
observer: {
lat: Number(startLat.toFixed(6)),
lng: Number(startLng.toFixed(6)),
heightOffsetMeters: obsHeightOffset,
groundElevationMeters: observerGroundElev,
totalElevationMeters: observerTotalElev,
},
target: {
lat: Number(endLat.toFixed(6)),
lng: Number(endLng.toFixed(6)),
heightOffsetMeters: tgtHeightOffset,
groundElevationMeters: targetGroundElev,
totalElevationMeters: targetTotalElev,
},
profile: compact ? [] : points,
};
return 650 + (lng - 36.0) * 30;
}
}
+9 -6
View File
@@ -5,6 +5,7 @@ import App from './App.tsx'
import CompareView from './pages/CompareView'
import IntelligenceDashboard from './pages/IntelligenceDashboard'
import { ExecutiveShowcase } from './pages/ExecutiveShowcase'
import { TacticalDefenseView } from './pages/TacticalDefenseView'
import './index.css'
maplibregl.setRTLTextPlugin(
@@ -15,14 +16,15 @@ maplibregl.setRTLTextPlugin(
// Lightweight hash router
const NAV = [
{ hash: '#executive', label: 'العرض الاستراتيجي', icon: '🎖️' },
{ hash: '#map', label: 'الخريطة الحية', icon: '🗺️' },
{ hash: '#tactical', label: 'المنظومة التكتيكية العسكرية', icon: '🎖️' },
{ hash: '#map', label: 'الخريطة والطقس', icon: '🗺️' },
{ hash: '#review', label: 'تدقيق الطرق', icon: '🔍' },
{ hash: '#compare', label: 'المقارنة', icon: '⚖️' },
{ hash: '#compare', label: 'المقارنة المرجعية', icon: '⚖️' },
{ hash: '#executive', label: 'العرض الاستراتيجي', icon: '🏛️' },
]
function ViewNav({ hash }: { hash: string }) {
const currentHash = hash || '#map'
const currentHash = hash || '#tactical'
return (
<div style={{
position: 'fixed',
@@ -72,14 +74,15 @@ function ViewNav({ hash }: { hash: string }) {
}
function Root() {
const [hash, setHash] = useState(window.location.hash || '#map')
const [hash, setHash] = useState(window.location.hash || '#tactical')
useEffect(() => {
const on = () => setHash(window.location.hash || '#map')
const on = () => setHash(window.location.hash || '#tactical')
window.addEventListener('hashchange', on)
return () => window.removeEventListener('hashchange', on)
}, [])
const view =
hash === '#tactical' || hash === '#military' ? <TacticalDefenseView /> :
hash === '#executive' || hash === '#pitch' ? <ExecutiveShowcase /> :
hash === '#compare' ? <CompareView /> :
hash === '#review' ? <IntelligenceDashboard /> :
+913
View File
@@ -0,0 +1,913 @@
import React, { useState, useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import {
Shield,
Crosshair,
Eye,
Radio,
Compass,
Mountain,
AlertTriangle,
CheckCircle2,
XCircle,
Layers,
Save,
Trash2,
Navigation,
Flame,
Activity,
Plus,
Play,
RotateCcw,
Zap,
ChevronDown
} from 'lucide-react';
import { calculateLineOfSight, calculateRadialViewshed, LineOfSightResult, Viewshed360Result } from '../utils/elevationService';
type TacticalMode = 'los' | 'viewshed' | 'artillery' | 'hlz' | 'symbols';
interface TacticalSymbolItem {
id: string;
type: 'friendly' | 'hostile' | 'artillery' | 'radar' | 'checkpoint' | 'minefield' | 'op';
name: string;
lat: number;
lng: number;
}
const DEFAULT_SYMBOLS: TacticalSymbolItem[] = [
{ id: 'sym-1', type: 'friendly', name: 'كتيبة المشاة الآلية 4', lat: 31.98, lng: 35.88 },
{ id: 'sym-2', type: 'artillery', name: 'مربض مدفعية م109', lat: 31.93, lng: 35.91 },
{ id: 'sym-3', type: 'radar', name: 'رادار كشف أرضي تكتيكي', lat: 32.02, lng: 35.84 },
{ id: 'sym-4', type: 'checkpoint', name: 'نقطة سيطرة وغلق أمني', lat: 31.95, lng: 35.95 },
{ id: 'sym-5', type: 'op', name: 'نقطة مراقبة واستطلاع أمامي', lat: 32.06, lng: 35.81 },
];
export const TacticalDefenseView: React.FC = () => {
const mapContainer = useRef<HTMLDivElement>(null);
const map = useRef<maplibregl.Map | null>(null);
const [mode, setMode] = useState<TacticalMode>('los');
const [cursorPos, setCursorPos] = useState({ lat: 31.95, lng: 35.93, elev: 850 });
// Line of Sight State
const [losPointA, setLosPointA] = useState<[number, number] | null>([31.9539, 35.9106]);
const [losPointB, setLosPointB] = useState<[number, number] | null>([32.0125, 35.8540]);
const [losObsHeight, setLosObsHeight] = useState<number>(2);
const [losTgtHeight, setLosTgtHeight] = useState<number>(2);
const [losResult, setLosResult] = useState<LineOfSightResult | null>(null);
const [losLoading, setLosLoading] = useState<boolean>(false);
// Viewshed State
const [viewshedCenter, setViewshedCenter] = useState<[number, number] | null>([31.9800, 35.8800]);
const [viewshedRadius, setViewshedRadius] = useState<number>(5000);
const [viewshedHeight, setViewshedHeight] = useState<number>(10);
const [viewshedResult, setViewshedResult] = useState<Viewshed360Result | null>(null);
const [viewshedLoading, setViewshedLoading] = useState<boolean>(false);
// Artillery Fire Mission State
const [gunPos, setGunPos] = useState<[number, number] | null>([31.9300, 35.9100]);
const [targetPos, setTargetPos] = useState<[number, number] | null>([32.0400, 35.8200]);
const [caliber, setCaliber] = useState<string>('155mm Howitzer (M109)');
const [muzzleVel, setMuzzleVel] = useState<number>(827);
const [artilleryResult, setArtilleryResult] = useState<any | null>(null);
const [artilleryLoading, setArtilleryLoading] = useState<boolean>(false);
// HLZ State
const [hlzCenter, setHlzCenter] = useState<[number, number] | null>([31.9600, 35.9000]);
const [hlzResult, setHlzResult] = useState<any | null>(null);
const [hlzLoading, setHlzLoading] = useState<boolean>(false);
// Tactical Symbols State
const [symbols, setSymbols] = useState<TacticalSymbolItem[]>(DEFAULT_SYMBOLS);
const [selectedSymbolType, setSelectedSymbolType] = useState<string>('friendly');
const [newSymbolName, setNewSymbolName] = useState<string>('');
// Map Initialization
useEffect(() => {
if (map.current) return;
if (!mapContainer.current) return;
const initialMap = new maplibregl.Map({
container: mapContainer.current,
style: '/style-dark.json',
center: [35.9106, 31.9539],
zoom: 11,
pitch: 35,
attributionControl: false
});
map.current = initialMap;
initialMap.on('mousemove', (e) => {
setCursorPos({
lat: Number(e.lngLat.lat.toFixed(5)),
lng: Number(e.lngLat.lng.toFixed(5)),
elev: 820 + Math.round(Math.sin(e.lngLat.lat * 20) * 150)
});
});
initialMap.on('click', (e) => {
const clickedLat = e.lngLat.lat;
const clickedLng = e.lngLat.lng;
// Handle map clicks based on active mode
if (mode === 'los') {
if (!losPointA) {
setLosPointA([clickedLat, clickedLng]);
} else if (!losPointB) {
setLosPointB([clickedLat, clickedLng]);
} else {
setLosPointA([clickedLat, clickedLng]);
setLosPointB(null);
}
} else if (mode === 'viewshed') {
setViewshedCenter([clickedLat, clickedLng]);
} else if (mode === 'artillery') {
if (!gunPos) {
setGunPos([clickedLat, clickedLng]);
} else if (!targetPos) {
setTargetPos([clickedLat, clickedLng]);
} else {
setTargetPos([clickedLat, clickedLng]);
}
} else if (mode === 'hlz') {
setHlzCenter([clickedLat, clickedLng]);
} else if (mode === 'symbols') {
const newSym: TacticalSymbolItem = {
id: `sym-${Date.now()}`,
type: selectedSymbolType as any,
name: newSymbolName || `نقطة ${selectedSymbolType}`,
lat: clickedLat,
lng: clickedLng
};
setSymbols(prev => [...prev, newSym]);
setNewSymbolName('');
}
});
initialMap.on('load', () => {
// Add Sources and Layers for Tactical Defense Visualization
initialMap.addSource('tactical-los-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
id: 'tactical-los-line',
type: 'line',
source: 'tactical-los-src',
filter: ['==', '$type', 'LineString'],
paint: {
'line-color': ['case', ['==', ['get', 'blocked'], true], '#ef4444', '#22c55e'],
'line-width': 4,
'line-dasharray': [2, 1]
}
});
initialMap.addLayer({
id: 'tactical-los-pts',
type: 'circle',
source: 'tactical-los-src',
filter: ['==', '$type', 'Point'],
paint: {
'circle-radius': 8,
'circle-color': ['match', ['get', 'role'], 'obs', '#38bdf8', 'tgt', '#f59e0b', 'obstacle', '#ef4444', '#ffffff'],
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 2
}
});
// Viewshed 360 Polygon Layer
initialMap.addSource('tactical-viewshed-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
id: 'tactical-viewshed-fill',
type: 'fill',
source: 'tactical-viewshed-src',
paint: {
'fill-color': '#22c55e',
'fill-opacity': 0.25
}
});
initialMap.addLayer({
id: 'tactical-viewshed-outline',
type: 'line',
source: 'tactical-viewshed-src',
paint: {
'line-color': '#4ade80',
'line-width': 2,
'line-dasharray': [3, 2]
}
});
// Artillery Trajectory Layer
initialMap.addSource('tactical-artillery-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
id: 'tactical-artillery-line',
type: 'line',
source: 'tactical-artillery-src',
paint: {
'line-color': '#f97316',
'line-width': 3.5
}
});
// Tactical Symbols Layer
initialMap.addSource('tactical-symbols-src', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
});
initialMap.addLayer({
id: 'tactical-symbols-pts',
type: 'circle',
source: 'tactical-symbols-src',
paint: {
'circle-radius': 9,
'circle-color': [
'match',
['get', 'type'],
'friendly', '#3b82f6',
'hostile', '#ef4444',
'artillery', '#f97316',
'radar', '#06b6d4',
'checkpoint', '#eab308',
'minefield', '#dc2626',
'#a855f7'
],
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 2.5
}
});
initialMap.addLayer({
id: 'tactical-symbols-lbls',
type: 'symbol',
source: 'tactical-symbols-src',
layout: {
'text-field': ['get', 'name'],
'text-size': 11,
'text-font': ['Noto Sans Bold', 'Open Sans Bold'],
'text-offset': [0, 1.4],
'text-anchor': 'top'
},
paint: {
'text-color': '#ffffff',
'text-halo-color': '#0f172a',
'text-halo-width': 2
}
});
});
return () => {
if (map.current) {
map.current.remove();
map.current = null;
}
};
}, []);
// Update Line of Sight Calculations & Map Layer
useEffect(() => {
if (!losPointA || !losPointB) return;
setLosLoading(true);
calculateLineOfSight(losPointA[0], losPointA[1], losPointB[0], losPointB[1], losObsHeight, losTgtHeight, 70)
.then((res) => {
setLosResult(res);
setLosLoading(false);
if (map.current && map.current.getSource('tactical-los-src')) {
const feats: any[] = [
{
type: 'Feature',
properties: { role: 'ray', blocked: !res.isDirectlyVisible },
geometry: {
type: 'LineString',
coordinates: [[losPointA[1], losPointA[0]], [losPointB[1], losPointB[0]]]
}
},
{
type: 'Feature',
properties: { role: 'obs', label: 'راصد' },
geometry: { type: 'Point', coordinates: [losPointA[1], losPointA[0]] }
},
{
type: 'Feature',
properties: { role: 'tgt', label: 'هدف' },
geometry: { type: 'Point', coordinates: [losPointB[1], losPointB[0]] }
}
];
if (res.highestObstacle) {
feats.push({
type: 'Feature',
properties: { role: 'obstacle', label: 'عائق' },
geometry: { type: 'Point', coordinates: [res.highestObstacle.lng, res.highestObstacle.lat] }
});
}
(map.current.getSource('tactical-los-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: feats
});
}
})
.catch(() => setLosLoading(false));
}, [losPointA, losPointB, losObsHeight, losTgtHeight]);
// Update 360 Viewshed Calculations & Map Layer
const runViewshed360 = () => {
if (!viewshedCenter) return;
setViewshedLoading(true);
calculateRadialViewshed(viewshedCenter[0], viewshedCenter[1], viewshedHeight, viewshedRadius, 36)
.then((res) => {
setViewshedResult(res);
setViewshedLoading(false);
if (map.current && map.current.getSource('tactical-viewshed-src')) {
(map.current.getSource('tactical-viewshed-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: [res.polygonGeoJson]
});
}
})
.catch(() => setViewshedLoading(false));
};
// Run Artillery Fire Mission
const runArtilleryMission = async () => {
if (!gunPos || !targetPos) return;
setArtilleryLoading(true);
try {
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
const apiKey = (import.meta as any).env.VITE_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
const res = await fetch(`${apiUrl}/tactical/artillery-fire-mission`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
body: JSON.stringify({
gunLat: gunPos[0],
gunLng: gunPos[1],
targetLat: targetPos[0],
targetLng: targetPos[1],
caliber,
muzzleVelocity: muzzleVel
})
});
const data = await res.json();
setArtilleryResult(data);
if (map.current && map.current.getSource('tactical-artillery-src')) {
const lineCoords = (data.trajectoryPoints || []).map((p: any) => [p.lng, p.lat]);
(map.current.getSource('tactical-artillery-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {},
geometry: { type: 'LineString', coordinates: lineCoords }
}
]
});
}
} catch (e) {
console.error('Artillery calculation error:', e);
} finally {
setArtilleryLoading(false);
}
};
// Run HLZ Assessment
const runHLZAssessment = async () => {
if (!hlzCenter) return;
setHlzLoading(true);
try {
const apiUrl = (import.meta as any).env.VITE_API_URL || '/api';
const apiKey = (import.meta as any).env.VITE_API_KEY || 'zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX';
const res = await fetch(`${apiUrl}/tactical/hlz-assessment?lat=${hlzCenter[0]}&lng=${hlzCenter[1]}&radius=4000`, {
headers: { 'x-api-key': apiKey }
});
const data = await res.json();
setHlzResult(data);
} catch (e) {
console.error('HLZ error:', e);
} finally {
setHlzLoading(false);
}
};
// Synchronize Tactical Symbols to Map
useEffect(() => {
if (!map.current || !map.current.getSource('tactical-symbols-src')) return;
const feats = symbols.map(s => ({
type: 'Feature',
properties: { type: s.type, name: s.name },
geometry: { type: 'Point', coordinates: [s.lng, s.lat] }
}));
(map.current.getSource('tactical-symbols-src') as maplibregl.GeoJSONSource).setData({
type: 'FeatureCollection',
features: feats as any
});
}, [symbols]);
return (
<div style={{
width: '100vw',
height: '100vh',
display: 'flex',
flexDirection: 'column',
background: '#0a0e17',
color: '#f8fafc',
fontFamily: "'IBM Plex Sans Arabic', Inter, system-ui, sans-serif",
direction: 'rtl',
overflow: 'hidden'
}}>
{/* Top Military HUD Header */}
<header style={{
height: 54,
background: 'rgba(11, 15, 25, 0.95)',
borderBottom: '1px solid rgba(255, 255, 255, 0.1)',
backdropFilter: 'blur(16px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 20px',
zIndex: 100,
gap: 16
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 34,
height: 34,
borderRadius: 8,
background: 'linear-gradient(135deg, #4f46e5, #06b6d4)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 0 16px rgba(79, 70, 229, 0.5)'
}}>
<Shield size={18} color="#ffffff" />
</div>
<div>
<div style={{ fontWeight: 800, fontSize: '0.95rem', letterSpacing: '-0.02em', display: 'flex', alignItems: 'center', gap: 8 }}>
<span>منظومة القيادة والسيطرة والتحليل التكتيكي</span>
<span style={{ fontSize: '10px', background: 'rgba(34, 197, 94, 0.2)', color: '#4ade80', padding: '1px 6px', borderRadius: 4 }}>
C4ISR Tactical Grid
</span>
</div>
</div>
</div>
{/* Tactical Modes Selector */}
<div style={{ display: 'flex', gap: 6, background: 'rgba(255, 255, 255, 0.05)', padding: 3, borderRadius: 10, border: '1px solid rgba(255,255,255,0.1)' }}>
{[
{ id: 'los', label: 'تبادل الرؤية (LOS)', icon: <Eye size={14} /> },
{ id: 'viewshed', label: 'كشف الميدان 360°', icon: <Radio size={14} /> },
{ id: 'artillery', label: 'رماية المدفعية', icon: <Flame size={14} /> },
{ id: 'hlz', label: 'مهابط المروحيات', icon: <Crosshair size={14} /> },
{ id: 'symbols', label: 'الرموز والعمليات', icon: <Layers size={14} /> },
].map(m => (
<button
key={m.id}
onClick={() => setMode(m.id as TacticalMode)}
style={{
background: mode === m.id ? 'linear-gradient(135deg, #4f46e5, #6366f1)' : 'transparent',
border: 'none',
color: mode === m.id ? '#ffffff' : '#94a3b8',
borderRadius: 7,
padding: '5px 12px',
fontSize: '0.8rem',
fontWeight: 700,
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: 5,
transition: 'all 0.15s ease'
}}
>
{m.icon}
<span>{m.label}</span>
</button>
))}
</div>
{/* Realtime Cursor HUD */}
<div style={{ display: 'flex', alignItems: 'center', gap: 16, fontSize: '0.75rem', fontFamily: 'monospace', color: '#94a3b8' }}>
<div>LAT: <span style={{ color: '#38bdf8' }}>{cursorPos.lat}</span></div>
<div>LNG: <span style={{ color: '#38bdf8' }}>{cursorPos.lng}</span></div>
<div>ELEV: <span style={{ color: '#4ade80' }}>{cursorPos.elev}m</span></div>
</div>
</header>
{/* Main Tactical Workspace */}
<div style={{ flex: 1, display: 'flex', position: 'relative', overflow: 'hidden' }}>
{/* Left Side Tactical Control Deck */}
<aside style={{
width: 360,
background: 'rgba(15, 23, 42, 0.9)',
backdropFilter: 'blur(20px)',
borderLeft: '1px solid rgba(255, 255, 255, 0.12)',
display: 'flex',
flexDirection: 'column',
zIndex: 20,
overflowY: 'auto',
padding: 16
}}>
{/* MODE 1: LINE OF SIGHT */}
{mode === 'los' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 10, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 800, color: '#38bdf8', display: 'flex', alignItems: 'center', gap: 8 }}>
<Eye size={18} /> تحليل خط النظر وتبادل الرؤية
</h3>
<p style={{ margin: '4px 0 0 0', fontSize: '0.78rem', color: '#94a3b8' }}>
اضغط على الخريطة لتحديد موقع الراصد (A) والهدف (B)
</p>
</div>
{/* Observer / Target Height Adjusters */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
<div>
<label style={{ fontSize: '0.75rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>ارتفاع الراصد (A):</label>
<select
value={losObsHeight}
onChange={e => setLosObsHeight(Number(e.target.value))}
style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.15)', color: '#fff', padding: '6px', borderRadius: 8, fontSize: '0.8rem' }}
>
<option value={2}>شخص واقف (2م)</option>
<option value={10}>آلية / برج مراقبة (10م)</option>
<option value={50}>سارية / درون استطلاع (50م)</option>
</select>
</div>
<div>
<label style={{ fontSize: '0.75rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>ارتفاع الهدف (B):</label>
<select
value={losTgtHeight}
onChange={e => setLosTgtHeight(Number(e.target.value))}
style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.15)', color: '#fff', padding: '6px', borderRadius: 8, fontSize: '0.8rem' }}
>
<option value={1.8}>شخص (1.8م)</option>
<option value={3}>مركبة / مدرعة (3م)</option>
<option value={15}>مبنى / محطة رادار (15م)</option>
</select>
</div>
</div>
{losLoading && <div style={{ color: '#38bdf8', fontSize: '0.85rem' }}>جاري حساب المقطع التضاريسي وشعاع الرؤية...</div>}
{losResult && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<div style={{
padding: '10px',
borderRadius: 10,
background: losResult.isDirectlyVisible ? 'rgba(34, 197, 94, 0.15)' : 'rgba(239, 68, 68, 0.15)',
border: losResult.isDirectlyVisible ? '1px solid #22c55e' : '1px solid #ef4444',
color: losResult.isDirectlyVisible ? '#4ade80' : '#f87171',
fontWeight: 700,
fontSize: '0.85rem',
display: 'flex',
alignItems: 'center',
gap: 8
}}>
{losResult.isDirectlyVisible ? <CheckCircle2 size={18} /> : <XCircle size={18} />}
<span>{losResult.isDirectlyVisible ? 'رؤية مباشرة مكشوفة بين الراصد والهدف' : 'الرؤية محجوبة خلف عائق تضاريسي'}</span>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 8, borderRadius: 8 }}>
<div style={{ fontSize: '0.7rem', color: '#94a3b8' }}>المسافة المباشرة</div>
<div style={{ fontSize: '1rem', fontWeight: 800 }}>{(losResult.totalDistance / 1000).toFixed(2)} كم</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 8, borderRadius: 8 }}>
<div style={{ fontSize: '0.7rem', color: '#94a3b8' }}>السمت / الاتجاه</div>
<div style={{ fontSize: '1rem', fontWeight: 800 }}>{losResult.azimuthDegrees}°</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 8, borderRadius: 8 }}>
<div style={{ fontSize: '0.7rem', color: '#94a3b8' }}>زاوية الموقع (Mils)</div>
<div style={{ fontSize: '1rem', fontWeight: 800, color: '#a78bfa' }}>{losResult.angleMils} mils</div>
</div>
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 8, borderRadius: 8 }}>
<div style={{ fontSize: '0.7rem', color: '#94a3b8' }}>الأرض الميتة</div>
<div style={{ fontSize: '1rem', fontWeight: 800, color: '#f87171' }}>{losResult.deadGroundPercentage}%</div>
</div>
</div>
{losResult.highestObstacle && (
<div style={{ padding: 10, background: 'rgba(239, 68, 68, 0.1)', borderRadius: 8, border: '1px solid rgba(239, 68, 68, 0.3)', fontSize: '0.78rem', color: '#fca5a5' }}>
<strong>العائق الحرج:</strong> على بعد {(losResult.highestObstacle.distance / 1000).toFixed(2)} كم، بارتفاع {losResult.highestObstacle.elevation}م (+{losResult.highestObstacle.excessHeight}م فوق خط النظر).
</div>
)}
</div>
)}
</div>
)}
{/* MODE 2: 360 VIEWSHED */}
{mode === 'viewshed' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 10, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 800, color: '#4ade80', display: 'flex', alignItems: 'center', gap: 8 }}>
<Radio size={18} /> كشف الميدان الدائري 360° (Viewshed)
</h3>
<p style={{ margin: '4px 0 0 0', fontSize: '0.78rem', color: '#94a3b8' }}>
حساب المساحات المكشوفة والميتة بزاوية 360 درجة حول الموقع
</p>
</div>
<div>
<label style={{ fontSize: '0.75rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>
نصف قطر الكشف: <strong>{(viewshedRadius / 1000).toFixed(1)} كم</strong>
</label>
<input
type="range"
min={1000}
max={12000}
step={500}
value={viewshedRadius}
onChange={e => setViewshedRadius(Number(e.target.value))}
style={{ width: '100%' }}
/>
</div>
<div>
<label style={{ fontSize: '0.75rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>
ارتفاع الرصد: <strong>{viewshedHeight} متر</strong>
</label>
<select
value={viewshedHeight}
onChange={e => setViewshedHeight(Number(e.target.value))}
style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.15)', color: '#fff', padding: '6px', borderRadius: 8, fontSize: '0.8rem' }}
>
<option value={2}>راصد أرضي (2م)</option>
<option value={10}>برج راداري / سارية (10م)</option>
<option value={30}>برج مراقبة حدودي (30م)</option>
</select>
</div>
<button
onClick={runViewshed360}
disabled={viewshedLoading}
style={{
background: 'linear-gradient(135deg, #10b981, #059669)',
color: '#fff',
border: 'none',
borderRadius: 10,
padding: '10px',
fontWeight: 700,
fontSize: '0.85rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
boxShadow: '0 4px 14px rgba(16, 185, 129, 0.4)'
}}
>
<Play size={16} />
{viewshedLoading ? 'جاري المسح الشعاعي...' : 'بدء كشف الميدان 360°'}
</button>
{viewshedResult && (
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 12, borderRadius: 10, border: '1px solid rgba(34, 197, 94, 0.2)' }}>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#4ade80', marginBottom: 8 }}>نتائج المسح التضاريسي:</div>
<div style={{ fontSize: '0.8rem', color: '#cbd5e1', lineHeight: 1.7 }}>
<div>• المساحة المكشوفة: <strong>{viewshedResult.visibleAreaKm2} كم²</strong></div>
<div>• نسبة الكشف البصري: <strong>{viewshedResult.visiblePercentage}%</strong> من المدى الكلي</div>
<div>• ارتفاع نقطة الرصد: <strong>{viewshedResult.centerElevation} م فوق سطح البحر</strong></div>
</div>
</div>
)}
</div>
)}
{/* MODE 3: ARTILLERY FIRE MISSION */}
{mode === 'artillery' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 10, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 800, color: '#f97316', display: 'flex', alignItems: 'center', gap: 8 }}>
<Flame size={18} /> حاسبة الرماية المدفعية والمسار البالستي
</h3>
<p style={{ margin: '4px 0 0 0', fontSize: '0.78rem', color: '#94a3b8' }}>
حساب زوايا الرماية (Mils) وفحص أمان عبور القذيفة فوق القمم
</p>
</div>
<div>
<label style={{ fontSize: '0.75rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>نوع السلاح والعيار:</label>
<select
value={caliber}
onChange={e => {
setCaliber(e.target.value);
if (e.target.value.includes('155mm')) setMuzzleVel(827);
else if (e.target.value.includes('122mm')) setMuzzleVel(690);
else setMuzzleVel(320); // Mortar
}}
style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.15)', color: '#fff', padding: '6px', borderRadius: 8, fontSize: '0.8rem' }}
>
<option value="155mm Howitzer (M109)">هاوتزر 155 ملم (M109 / M777)</option>
<option value="122mm D-30">مدفعية 122 ملم (D-30)</option>
<option value="120mm Heavy Mortar">هاون ثقيل 120 ملم</option>
</select>
</div>
<button
onClick={runArtilleryMission}
disabled={artilleryLoading}
style={{
background: 'linear-gradient(135deg, #f97316, #ea580c)',
color: '#fff',
border: 'none',
borderRadius: 10,
padding: '10px',
fontWeight: 700,
fontSize: '0.85rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
boxShadow: '0 4px 14px rgba(249, 115, 22, 0.4)'
}}
>
<Flame size={16} />
{artilleryLoading ? 'جاري الحساب البالستي...' : 'حساب مهمة الرماية (Fire Mission)'}
</button>
{artilleryResult && (
<div style={{ background: 'rgba(255,255,255,0.04)', padding: 12, borderRadius: 10, border: '1px solid rgba(249, 115, 22, 0.3)', fontSize: '0.8rem', lineHeight: 1.7 }}>
<div style={{ fontWeight: 800, color: '#fb923c', marginBottom: 6 }}>بيانات الرماية (Firing Solution):</div>
<div>• المسافة للهدف: <strong>{artilleryResult.distanceKm} كم</strong> ({artilleryResult.distanceMeters}م)</div>
<div>• السمت (Azimuth): <strong>{artilleryResult.azimuthDegrees}°</strong> ({artilleryResult.azimuthMils} mils)</div>
<div>• زاوية الرمي المنخفضة (QE Low): <strong style={{ color: '#a78bfa' }}>{artilleryResult.lowAngle?.mils} mils</strong> ({artilleryResult.lowAngle?.degrees}°)</div>
<div>• زاوية الرمي العالية (QE High): <strong style={{ color: '#a78bfa' }}>{artilleryResult.highAngle?.mils} mils</strong></div>
<div>• زمن طيران القذيفة (TOF): <strong>{artilleryResult.timeOfFlightSeconds} ثانية</strong></div>
<div>• أعلى نقطة للمسار (Apex): <strong>{artilleryResult.apexAltitudeMeters} م</strong></div>
<div style={{
marginTop: 8,
padding: 6,
borderRadius: 6,
background: artilleryResult.hasCrestClearance ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)',
color: artilleryResult.hasCrestClearance ? '#4ade80' : '#f87171',
fontWeight: 700
}}>
{artilleryResult.hasCrestClearance ? '✓ مسار القذيفة آمن ويعلو كافة القمم' : '✕ تحذير: القذيفة تصطدم بقمة وسيطة!'}
</div>
</div>
)}
</div>
)}
{/* MODE 4: HLZ ASSESSMENT */}
{mode === 'hlz' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 10, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 800, color: '#06b6d4', display: 'flex', alignItems: 'center', gap: 8 }}>
<Crosshair size={18} /> كشف مهابط المروحيات والإخلاء الطبي
</h3>
<p style={{ margin: '4px 0 0 0', fontSize: '0.78rem', color: '#94a3b8' }}>
مسح تضاريسي لاستخراج الأراضي المستوية والصالحة لهبوط الطيران العمودي
</p>
</div>
<button
onClick={runHLZAssessment}
disabled={hlzLoading}
style={{
background: 'linear-gradient(135deg, #06b6d4, #0891b2)',
color: '#fff',
border: 'none',
borderRadius: 10,
padding: '10px',
fontWeight: 700,
fontSize: '0.85rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
boxShadow: '0 4px 14px rgba(6, 182, 212, 0.4)'
}}
>
<Crosshair size={16} />
{hlzLoading ? 'جاري مسح التضاريس...' : 'كشف المهابط ضمن 4 كم'}
</button>
{hlzResult && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#38bdf8' }}>
تم العثور على {hlzResult.suitableCount} مهابط صالحة:
</div>
{hlzResult.zones.slice(0, 5).map((z: any) => (
<div key={z.id} style={{
padding: 8,
borderRadius: 8,
background: z.suitability === 'EXCELLENT' ? 'rgba(34,197,94,0.1)' : 'rgba(255,255,255,0.03)',
border: z.suitability === 'EXCELLENT' ? '1px solid rgba(34,197,94,0.3)' : '1px solid rgba(255,255,255,0.08)',
fontSize: '0.78rem'
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontWeight: 700 }}>
<span>{z.id} ({z.distanceMeters}م)</span>
<span style={{ color: z.suitability === 'EXCELLENT' ? '#4ade80' : '#fbbf24' }}>{z.suitability_ar}</span>
</div>
<div style={{ color: '#94a3b8', marginTop: 2 }}>
انحدار: {z.slopeDegrees}° | قطر المروحة: {z.maxRotorDiameterMeters}م
</div>
</div>
))}
</div>
)}
</div>
)}
{/* MODE 5: TACTICAL SYMBOLOGY */}
{mode === 'symbols' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<div style={{ paddingBottom: 10, borderBottom: '1px solid rgba(255,255,255,0.08)' }}>
<h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 800, color: '#a855f7', display: 'flex', alignItems: 'center', gap: 8 }}>
<Layers size={18} /> الرموز التكتيكية والسيطرة الميدانية
</h3>
<p style={{ margin: '4px 0 0 0', fontSize: '0.78rem', color: '#94a3b8' }}>
اختر الرمز العسكري واضغط على الخريطة لتثبيت مواقع التشكيلات
</p>
</div>
<div>
<label style={{ fontSize: '0.75rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>نوع الرمز العسكري:</label>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
{[
{ id: 'friendly', label: '🟦 وحدة صديقة' },
{ id: 'hostile', label: '🟥 هدف معادي' },
{ id: 'artillery', label: '🟧 مربض مدفعية' },
{ id: 'radar', label: '🟦 محطة رادار' },
{ id: 'checkpoint', label: '🟨 نقطة سيطرة' },
{ id: 'op', label: '🟪 نقطة مراقبة' },
].map(sym => (
<button
key={sym.id}
onClick={() => setSelectedSymbolType(sym.id)}
style={{
background: selectedSymbolType === sym.id ? 'rgba(168, 85, 247, 0.25)' : 'rgba(255,255,255,0.05)',
border: selectedSymbolType === sym.id ? '1px solid #a855f7' : '1px solid rgba(255,255,255,0.1)',
color: '#fff',
padding: '6px 8px',
borderRadius: 6,
fontSize: '0.75rem',
cursor: 'pointer',
textAlign: 'right'
}}
>
{sym.label}
</button>
))}
</div>
</div>
<div>
<label style={{ fontSize: '0.75rem', color: '#94a3b8', display: 'block', marginBottom: 4 }}>تسمية الرمز / الوحدة:</label>
<input
type="text"
placeholder="مثال: سرية الدبابات 1"
value={newSymbolName}
onChange={e => setNewSymbolName(e.target.value)}
style={{ width: '100%', background: 'rgba(255,255,255,0.06)', border: '1px solid rgba(255,255,255,0.15)', color: '#fff', padding: '8px', borderRadius: 8, fontSize: '0.8rem' }}
/>
</div>
{/* Active Symbols List */}
<div style={{ marginTop: 8 }}>
<div style={{ fontSize: '0.8rem', color: '#94a3b8', marginBottom: 6 }}>الرموز المنتشرة على الخريطة ({symbols.length}):</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, maxHeight: 180, overflowY: 'auto' }}>
{symbols.map(s => (
<div key={s.id} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', background: 'rgba(255,255,255,0.04)', padding: '6px 10px', borderRadius: 6, fontSize: '0.78rem' }}>
<span>{s.name}</span>
<button
onClick={() => setSymbols(symbols.filter(x => x.id !== s.id))}
style={{ background: 'none', border: 'none', color: '#ef4444', cursor: 'pointer', padding: 2 }}
>
✕
</button>
</div>
))}
</div>
</div>
</div>
)}
</aside>
{/* Tactical Map Container */}
<div ref={mapContainer} style={{ flex: 1, height: '100%', position: 'relative' }} />
</div>
</div>
);
};
+104
View File
@@ -361,3 +361,107 @@ export async function calculateLineOfSight(
azimuthDegrees: Math.round(azimuthDegrees * 10) / 10,
};
}
export interface Viewshed360Result {
centerLat: number;
centerLng: number;
centerElevation: number;
radiusMeters: number;
polygonGeoJson: any;
totalRays: number;
visibleAreaKm2: number;
visiblePercentage: number;
}
/**
* Calculates 360-degree Radial Viewshed (كشف الميدان الدائري) around observer
*/
export async function calculateRadialViewshed(
centerLat: number,
centerLng: number,
obsHeight: number = 2,
radiusMeters: number = 5000,
numRays: number = 36
): Promise<Viewshed360Result> {
const centerGroundElev = await sampleElevationAt(centerLat, centerLng);
const observerElevation = centerGroundElev + obsHeight;
const R_earth = 6371000;
const k_refraction = 0.13;
const effectiveEarthRadius = R_earth / (1 - k_refraction);
const polygonCoordinates: [number, number][] = [];
const samplesPerRay = 12;
let totalVisibleDistanceSum = 0;
for (let rayIdx = 0; rayIdx < numRays; rayIdx++) {
const angleDeg = (rayIdx * 360) / numRays;
const angleRad = (angleDeg * Math.PI) / 180;
const dLat = (radiusMeters / R_earth) * (180 / Math.PI) * Math.cos(angleRad);
const dLng = (radiusMeters / (R_earth * Math.cos((centerLat * Math.PI) / 180))) * (180 / Math.PI) * Math.sin(angleRad);
const endLat = centerLat + dLat;
const endLng = centerLng + dLng;
let maxAngleSoFar = -Infinity;
let visibleHorizonDist = radiusMeters;
let visibleHorizonLat = endLat;
let visibleHorizonLng = endLng;
for (let step = 1; step <= samplesPerRay; step++) {
const frac = step / samplesPerRay;
const sLat = centerLat + (endLat - centerLat) * frac;
const sLng = centerLng + (endLng - centerLng) * frac;
const sDist = radiusMeters * frac;
const sElev = await sampleElevationAt(sLat, sLng);
const earthCurvatureDrop = (sDist * sDist) / (2 * effectiveEarthRadius);
const apparentElev = sElev - earthCurvatureDrop;
const angle = (apparentElev - observerElevation) / sDist;
if (angle >= maxAngleSoFar) {
maxAngleSoFar = angle;
visibleHorizonDist = sDist;
visibleHorizonLat = sLat;
visibleHorizonLng = sLng;
}
}
totalVisibleDistanceSum += visibleHorizonDist;
polygonCoordinates.push([visibleHorizonLng, visibleHorizonLat]);
}
if (polygonCoordinates.length > 0) {
polygonCoordinates.push(polygonCoordinates[0]);
}
const polygonGeoJson = {
type: 'Feature',
properties: {
centerLat,
centerLng,
radiusMeters
},
geometry: {
type: 'Polygon',
coordinates: [polygonCoordinates]
}
};
const avgVisibleDist = totalVisibleDistanceSum / numRays;
const theoreticalMaxArea = Math.PI * Math.pow(radiusMeters / 1000, 2);
const actualVisibleArea = Math.PI * Math.pow(avgVisibleDist / 1000, 2);
const visiblePercentage = Math.min(100, Math.round((actualVisibleArea / theoreticalMaxArea) * 100));
return {
centerLat,
centerLng,
centerElevation: Math.round(observerElevation),
radiusMeters,
polygonGeoJson,
totalRays: numRays,
visibleAreaKm2: Math.round(actualVisibleArea * 10) / 10,
visiblePercentage
};
}