diff --git a/src/atc/atc.service.ts b/src/atc/atc.service.ts index bbba4d4..a2608f0 100644 --- a/src/atc/atc.service.ts +++ b/src/atc/atc.service.ts @@ -6,7 +6,7 @@ import { IvaoService } from '../utilities/ivao.service'; @Injectable() export class AtcService { constructor(private readonly vatsimService: VatsimService, - private readonly ivaoService: IvaoService) { } + private readonly ivaoService: IvaoService) {} public async getVatsimControllers(): Promise { const data = await this.vatsimService.fetchVatsimData(); @@ -46,36 +46,64 @@ export class AtcService { } public async getIvaoControllers(): Promise { - const { clients: { atcs } } = await this.ivaoService.fetchIvaoData(); - - return atcs.map((atc) => ({ - callsign: atc.callsign, - frequency: atc.atcSession.frequency.toString(), - textAtis: atc.atis?.lines, - latitude: atc.lastTrack.latitude, - longitude: atc.lastTrack.longitude, - type: this.callSignToAtcType(atc.callsign), - // TODO FIXME: visual range is not currently available in the new IVAO Whazzup v2 data - visualRange: 100, - })); + const data = await this.ivaoService.fetchIvaoData(); + + const arr: ATCInfo[] = []; + for (const c of data) { + if (c.indexOf(':ATC:') > -1) { + const split = c.split(':'); + if (split[0].indexOf('_') > -1) { + const atisLine = data.find((x) => x.startsWith(`${split[0].split('_')[0]}_TWR`)); + const atis = atisLine?.split(':')[35] + .split('^§') + .slice(1) + .join(' ') + .toUpperCase(); + + arr.push({ + callsign: split[0], + frequency: split[4], + textAtis: atis ? [atis] : null, + visualRange: parseInt(split[19]), + type: this.callSignToAtcType(split[0]), + latitude: parseFloat(split[5]), + longitude: parseFloat(split[6]), + }); + } + } + } + + return arr.filter((c) => c.type !== AtcType.UNKNOWN); } public callSignToAtcType(callsign: string): AtcType { - switch (callsign.split('_').reverse()[0]) { - case 'CTR': return AtcType.RADAR; - case 'DEL': return AtcType.DELIVERY; - case 'GND': return AtcType.GROUND; - case 'DEP': return AtcType.DEPARTURE; - case 'TWR': return AtcType.TOWER; - case 'APP': return AtcType.APPROACH; - case 'ATIS': return AtcType.ATIS; - default: return AtcType.UNKNOWN; + if (callsign.indexOf('_CTR') > -1) { + return AtcType.RADAR; + } + if (callsign.indexOf('_DEL') > -1) { + return AtcType.DELIVERY; + } + if (callsign.indexOf('_GND') > -1) { + return AtcType.GROUND; + } + if (callsign.indexOf('_DEP') > -1) { + return AtcType.DEPARTURE; + } + if (callsign.indexOf('_APP') > -1) { + return AtcType.APPROACH; + } + if (callsign.indexOf('_TWR') > -1) { + return AtcType.TOWER; + } + if (callsign.indexOf('_ATIS') > -1) { + return AtcType.ATIS; } + return AtcType.UNKNOWN; } - public getFrequency(array: any[]): string { + public getFrequency(array:any[]):string { if (array && array.length > 0 && array[0].frequency) { - let freqInString: string = array[0].frequency.toString(); + let freqInString : string = array[0].frequency.toString(); freqInString = `${freqInString.substr(0, 3)}.${freqInString.substr(3)}`; return parseFloat(freqInString).toFixed(3); } diff --git a/src/atis/atis.service.ts b/src/atis/atis.service.ts index 790007f..0940574 100644 --- a/src/atis/atis.service.ts +++ b/src/atis/atis.service.ts @@ -8,128 +8,120 @@ import { IvaoService } from '../utilities/ivao.service'; @Injectable() export class AtisService { - private readonly logger = new Logger(AtisService.name); + private readonly logger = new Logger(AtisService.name); - constructor( - private http: HttpService, - private readonly cache: CacheService, - private readonly vatsim: VatsimService, - private readonly ivao: IvaoService, - ) {} + constructor(private http: HttpService, + private readonly cache: CacheService, + private readonly vatsim: VatsimService, + private readonly ivao : IvaoService) { + } - getForICAO(icao: string, source?: string): Promise { - const icaoCode = icao.toUpperCase(); - this.logger.debug( - `Searching for ICAO ${icaoCode} from source ${source}`, - ); + getForICAO(icao: string, source?: string): Promise { + const icaoCode = icao.toUpperCase(); + this.logger.debug(`Searching for ICAO ${icaoCode} from source ${source}`); - switch (source?.toLowerCase()) { - case 'faa': - default: - return this.handleFaa(icaoCode).toPromise(); - case 'vatsim': - return this.handleVatsim(icaoCode); - case 'ivao': - return this.handleIvao(icaoCode); - case 'pilotedge': - return this.handlePilotEdge(icaoCode).toPromise(); - } - } + switch (source?.toLowerCase()) { + case 'faa': + default: + return this.handleFaa(icaoCode).toPromise(); + case 'vatsim': + return this.handleVatsim(icaoCode); + case 'ivao': + return this.handleIvao(icaoCode); + case 'pilotedge': + return this.handlePilotEdge(icaoCode).toPromise(); + } + } - // FAA - private handleFaa(icao: string): Observable { - return this.http.get(`https://datis.clowd.io/api/${icao}`).pipe( - tap((response) => this.logger.debug( - `Response status ${response.status} for FAA ATIS request`, - )), - map((response) => { - if (response.data.error) { - throw this.generateNotAvailableException( - response.data, - icao, - ); - } + // FAA + private handleFaa(icao: string): Observable { + return this.http.get(`https://datis.clowd.io/api/${icao}`) + .pipe( + tap((response) => this.logger.debug(`Response status ${response.status} for FAA ATIS request`)), + map((response) => { + if (response.data.error) { + throw this.generateNotAvailableException(response.data, icao); + } - const atis: Atis = { - icao, - source: 'FAA', - }; + const atis: Atis = { + icao, + source: 'FAA', + }; - response.data.forEach((x) => { - atis[x.type] = x.datis; - }); + response.data.forEach((x) => { + atis[x.type] = x.datis; + }); - atis.dep?.toUpperCase(); - atis.arr?.toUpperCase(); - atis.combined?.toUpperCase(); + atis.dep?.toUpperCase(); + atis.arr?.toUpperCase(); + atis.combined?.toUpperCase(); - return atis; - }), - catchError((err) => { - throw this.generateNotAvailableException(err, icao); - }), - ); - } + return atis; + }), + catchError( + (err) => { + throw this.generateNotAvailableException(err, icao); + }, + ), + ); + } - private handleVatsim(icao: string): Promise { - return this.vatsim - .fetchVatsimData() - .then((response) => ({ - icao, - source: 'Vatsim', - combined: response.atis - .find((x) => x.callsign === `${icao}_ATIS`) - .text_atis.join(' ') - .toUpperCase(), - })) - .catch((e) => { - throw this.generateNotAvailableException(e, icao); - }); - } + private handleVatsim(icao: string): Promise { + return this.vatsim.fetchVatsimData() + .then((response) => ({ + icao, + source: 'Vatsim', + combined: response.atis + .find((x) => x.callsign === `${icao}_ATIS`) + .text_atis + .join(' ') + .toUpperCase(), + })) + .catch((e) => { + throw this.generateNotAvailableException(e, icao); + }); + } - private handleIvao(icao: string): Promise { - return this.ivao - .fetchIvaoData() - .then((response) => ({ - icao, - source: 'IVAO', - combined: response.clients.atcs - .find((atc) => atc.callsign.includes(icao.toUpperCase())) - ?.atis?.lines.join(' '), - })) - .catch((e) => { - throw this.generateNotAvailableException(e, icao); - }); - } + private handleIvao(icao: string): Promise { + return this.ivao.fetchIvaoData() + .then((response) => ({ + icao, + source: 'IVAO', + combined: response + .find((x) => x.startsWith(`${icao}_TWR`)) + .split(':')[35] + .split('^§') + .slice(1) + .join(' ') + .toUpperCase(), + })) + .catch((e) => { + throw this.generateNotAvailableException(e, icao); + }); + } - // PilotEdge - private handlePilotEdge(icao: string): Observable { - return this.http - .get(`https://www.pilotedge.net/atis/${icao}.json`) - .pipe( - tap((response) => this.logger.debug( - `Response status ${response.status} for PilotEdge ATIS request`, - )), - map((response) => ({ - icao, - source: 'FAA', - combined: response.data.text - .replace('\n\n', ' ') - .toUpperCase(), - })), - catchError((err) => { - throw this.generateNotAvailableException(err, icao); - }), - ); - } + // PilotEdge + private handlePilotEdge(icao: string): Observable { + return this.http.get(`https://www.pilotedge.net/atis/${icao}.json`) + .pipe( + tap((response) => this.logger.debug(`Response status ${response.status} for PilotEdge ATIS request`)), + map((response) => ({ + icao, + source: 'FAA', + combined: response.data.text.replace('\n\n', ' ').toUpperCase(), + })), + catchError( + (err) => { + throw this.generateNotAvailableException(err, icao); + }, + ), + ); + } - private generateNotAvailableException(err: any, icao: string) { - const exception = new HttpException( - `ATIS not available for ICAO: ${icao}`, - 404, - ); - this.logger.error(err); - this.logger.error(exception); - return exception; - } + private generateNotAvailableException(err: any, icao: string) { + const exception = new HttpException(`ATIS not available for ICAO: ${icao}`, 404); + this.logger.error(err); + this.logger.error(exception); + return exception; + } } diff --git a/src/utilities/ivao.service.ts b/src/utilities/ivao.service.ts index b10384f..c447859 100644 --- a/src/utilities/ivao.service.ts +++ b/src/utilities/ivao.service.ts @@ -2,127 +2,37 @@ import { HttpService, Injectable, Logger } from '@nestjs/common'; import { map, tap } from 'rxjs/operators'; import { CacheService } from '../cache/cache.service'; -interface Client { - time: number; - id: number; - userId: number; - callsign: string; - serverId: string; - softwareVersion: string; - softwareTypeId: string; - rating: number; - lastTrack: { - altitude: number; - altitudeDifference: number; - arrivalDistance: number | null; - departureDistance: number | null; - groundSpeed: number; - heading: number; - latitude: number; - longitude: number; - onGround: boolean; - state: null; - time: number; - timestamp: string; - transponder: number; - transponderMode: string; - }; - atcSession: { - frequency: number; - position: string; - }; - createdAt: string; -} - -interface Server { - id: string; - hostname: string; - ip: string; - description: string; - countryId: string; - currentConnections: number; - maximumConnections: number; -} - -interface Atis { - lines: string[]; - callsign: string; - revision: number; - timestamp: string; - sessionId: number; -} - -interface Whazzup { - updatedAt: string; - servers: Server[]; - voiceServers: Server[]; - clients: { - atcs: Client[]; - followMe: unknown[]; - observers: Client[]; - pilots: Client[]; - }; - connections: { - atc: number; - observer: number; - pilot: number; - supervisor: number; - total: number; - worldTour: number; - }; -} - -interface AtisMergedWhazzup extends Whazzup { - clients: { - atcs: (Client & { atis?: Atis })[]; - followMe: unknown[]; - observers: Client[]; - pilots: Client[]; - } -} - -type WhazzupAtis = Atis[]; +// TODO: investigate +// For some reason iconv is not working with import +// eslint-disable-next-line @typescript-eslint/no-var-requires +const iconv = require('iconv-lite'); @Injectable() export class IvaoService { - private readonly logger = new Logger(IvaoService.name); - - constructor( - private http: HttpService, - private readonly cache: CacheService, - ) {} - - public async fetchIvaoData(): Promise { - const cacheHit = await this.cache.get('/ivao/data'); - - if (cacheHit) { - this.logger.debug('Returning from cache'); - return cacheHit; - } - - const whazzupData: Whazzup = await this.http.get('https://api.ivao.aero/v2/tracker/whazzup') - .pipe( - tap((response) => this.logger.debug(`Response status ${response.status} for IVAO request`)), - tap((response) => this.logger.debug(`Response contains ${response.data.length} entries`)), - map((response) => response.data), - ).toPromise(); - - const whazzupAtisData: WhazzupAtis = await this.http.get('https://api.ivao.aero/v2/tracker/whazzup/atis') - .pipe( - tap((response) => this.logger.debug(`Response status ${response.status} for IVAO request`)), - tap((response) => this.logger.debug(`Response contains ${response.data.length} entries`)), - map((response) => response.data), - ).toPromise(); - - for (const atis of whazzupAtisData) { - const atcWithSameCallsign = (whazzupData as AtisMergedWhazzup).clients.atcs.find((atcs) => atcs.callsign === atis.callsign); - - if (atcWithSameCallsign) { - atcWithSameCallsign.atis = atis; - } - } - - this.cache.set('/ivao/data', whazzupData, 120).then(); - return whazzupData; - } + private readonly logger = new Logger(IvaoService.name); + + constructor( + private http: HttpService, + private readonly cache: CacheService, + ) {} + + public async fetchIvaoData(): Promise { + const cacheHit = await this.cache.get('/ivao/data'); + + if (cacheHit) { + this.logger.debug('Returning from cache'); + return cacheHit; + } + const data = await this.http.get('https://api.ivao.aero/getdata/whazzup/whazzup.txt', { responseType: 'arraybuffer' }) + .pipe( + tap((response) => this.logger.debug(`Response status ${response.status} for IVAO request`)), + tap((response) => this.logger.debug(`Response contains ${response.data.length} entries`)), + map((response) => iconv + .decode(response.data, 'ISO-8859-1') + .split(/\r?\n/)), + ).toPromise(); + + this.cache.set('/ivao/data', data, 120).then(); + return data; + } }