Add ADSB Exchange
This commit is contained in:
@@ -38,6 +38,7 @@
|
|||||||
"cache-manager": "^5.3.1",
|
"cache-manager": "^5.3.1",
|
||||||
"cache-manager-redis-yet": "^4.1.2",
|
"cache-manager-redis-yet": "^4.1.2",
|
||||||
"fp-ts": "^2.16.3",
|
"fp-ts": "^2.16.3",
|
||||||
|
"haversine-ts": "^1.2.0",
|
||||||
"hbs": "^4.2.0",
|
"hbs": "^4.2.0",
|
||||||
"ioredis": "^5.3.2",
|
"ioredis": "^5.3.2",
|
||||||
"minio": "^7.1.3",
|
"minio": "^7.1.3",
|
||||||
|
116
src/adsb-exchange/adsb-exchange.controller.ts
Normal file
116
src/adsb-exchange/adsb-exchange.controller.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Logger,
|
||||||
|
Param,
|
||||||
|
UseInterceptors,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ExtendedAircraft, LiteAircraft } from './types';
|
||||||
|
import { AdsbExchangeService, ValidRadius } from './adsb-exchange.service';
|
||||||
|
import { CacheInterceptor, CacheTTL } from '@nestjs/cache-manager';
|
||||||
|
import { ApiParam, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
@Controller('adsb-exchange')
|
||||||
|
@UseInterceptors(CacheInterceptor)
|
||||||
|
@CacheTTL(60)
|
||||||
|
@ApiTags('adsb-exchange')
|
||||||
|
export class AdsbExchangeController {
|
||||||
|
private readonly logger: Logger = new Logger(AdsbExchangeController.name);
|
||||||
|
constructor(private readonly adsbExchangeService: AdsbExchangeService) {}
|
||||||
|
|
||||||
|
@Get('radius/lat/:lat/long/:long/dist/:dist')
|
||||||
|
@ApiParam({
|
||||||
|
name: 'lat',
|
||||||
|
type: Number,
|
||||||
|
example: 40.6043,
|
||||||
|
required: true,
|
||||||
|
description: 'Latitude of the center point',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'long',
|
||||||
|
type: Number,
|
||||||
|
example: -105.01919,
|
||||||
|
required: true,
|
||||||
|
description: 'Longitude of the center point, note that West is negative',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'dist',
|
||||||
|
type: Number,
|
||||||
|
example: 10,
|
||||||
|
required: true,
|
||||||
|
description:
|
||||||
|
'Radius in nautical miles. Must be one of 1, 5, 10, 25, 50, 100, 250',
|
||||||
|
})
|
||||||
|
async adsbExchangeAircraftWithinRadius(
|
||||||
|
@Param('lat') lat: number,
|
||||||
|
@Param('long') long: number,
|
||||||
|
@Param('dist') dist: number,
|
||||||
|
): Promise<ExtendedAircraft[]> {
|
||||||
|
dist = Number(dist);
|
||||||
|
lat = Number(lat);
|
||||||
|
long = Number(long);
|
||||||
|
this.logger.verbose(
|
||||||
|
`Requesting aircraft within ${dist} nautical miles of ${lat}, ${long}`,
|
||||||
|
);
|
||||||
|
if (!this.adsbExchangeService.isValidRadius(dist)) {
|
||||||
|
throw new BadRequestException(`Invalid radius: ${dist}`);
|
||||||
|
}
|
||||||
|
return this.adsbExchangeService.extendAircraft(
|
||||||
|
await this.adsbExchangeService.adsbExchangeAircraftWithinRadius(dist, {
|
||||||
|
lat,
|
||||||
|
long,
|
||||||
|
}),
|
||||||
|
{ lat, long },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('radius/lat/:lat/long/:long/dist/:dist/lite')
|
||||||
|
@ApiParam({
|
||||||
|
name: 'lat',
|
||||||
|
type: Number,
|
||||||
|
example: 40.6043,
|
||||||
|
required: true,
|
||||||
|
description: 'Latitude of the center point',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'long',
|
||||||
|
type: Number,
|
||||||
|
example: -105.01919,
|
||||||
|
required: true,
|
||||||
|
description: 'Longitude of the center point, note that West is negative',
|
||||||
|
})
|
||||||
|
@ApiParam({
|
||||||
|
name: 'dist',
|
||||||
|
type: Number,
|
||||||
|
example: 10,
|
||||||
|
required: true,
|
||||||
|
description:
|
||||||
|
'Radius in nautical miles. Must be one of 1, 5, 10, 25, 50, 100, 250',
|
||||||
|
})
|
||||||
|
async adsbExchangeAircraftWithinRadiusLite(
|
||||||
|
@Param('lat') lat: number,
|
||||||
|
@Param('long') long: number,
|
||||||
|
@Param('dist') dist: number,
|
||||||
|
): Promise<LiteAircraft[]> {
|
||||||
|
dist = Number(dist);
|
||||||
|
lat = Number(lat);
|
||||||
|
long = Number(long);
|
||||||
|
this.logger.verbose(
|
||||||
|
`Requesting aircraft within ${dist} nautical miles of ${lat}, ${long}`,
|
||||||
|
);
|
||||||
|
if (!this.adsbExchangeService.isValidRadius(dist)) {
|
||||||
|
throw new BadRequestException(`Invalid radius: ${dist}`);
|
||||||
|
}
|
||||||
|
const aircraft = await this.adsbExchangeService.extendAircraft(
|
||||||
|
await this.adsbExchangeService.adsbExchangeAircraftWithinRadius(dist, {
|
||||||
|
lat,
|
||||||
|
long,
|
||||||
|
}),
|
||||||
|
{ lat, long },
|
||||||
|
);
|
||||||
|
return this.adsbExchangeService
|
||||||
|
.liteAircraft(aircraft)
|
||||||
|
.filter((aircraft) => aircraft.registration !== undefined);
|
||||||
|
}
|
||||||
|
}
|
11
src/adsb-exchange/adsb-exchange.module.ts
Normal file
11
src/adsb-exchange/adsb-exchange.module.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AdsbExchangeService } from './adsb-exchange.service';
|
||||||
|
import { AdsbExchangeController } from './adsb-exchange.controller';
|
||||||
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [CacheModule.register()],
|
||||||
|
providers: [AdsbExchangeService],
|
||||||
|
controllers: [AdsbExchangeController],
|
||||||
|
})
|
||||||
|
export class AdsbExchangeModule {}
|
81
src/adsb-exchange/adsb-exchange.service.ts
Normal file
81
src/adsb-exchange/adsb-exchange.service.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { AdsbExchange, Bearing, ExtendedAircraft } from './types';
|
||||||
|
import { DDPoint, Haversine, UnitOfDistance } from 'haversine-ts';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
const ValidRadii = [1, 5, 10, 25, 50, 100, 250] as const;
|
||||||
|
export type ValidRadius = (typeof ValidRadii)[number];
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdsbExchangeService {
|
||||||
|
private readonly logger: Logger = new Logger(AdsbExchangeService.name);
|
||||||
|
|
||||||
|
constructor(private readonly configService: ConfigService) {}
|
||||||
|
|
||||||
|
isValidRadius(radius: number): radius is ValidRadius {
|
||||||
|
return ValidRadii.includes(radius as ValidRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
bearingToAircraft(
|
||||||
|
aircraft: AdsbExchange.Aircraft,
|
||||||
|
location: AdsbExchange.Location,
|
||||||
|
): Bearing {
|
||||||
|
const locationPoint = new DDPoint(location.lat, location.long);
|
||||||
|
const aircraftPoint = new DDPoint(aircraft.lat, aircraft.lon);
|
||||||
|
|
||||||
|
const haversine = new Haversine(UnitOfDistance.Mile);
|
||||||
|
const bearing = haversine.getBearing(locationPoint, aircraftPoint);
|
||||||
|
return {
|
||||||
|
bearing: bearing.start,
|
||||||
|
distance: haversine.getDistance(locationPoint, aircraftPoint),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async adsbExchangeAircraftWithinRadius(
|
||||||
|
radius: ValidRadius,
|
||||||
|
location: AdsbExchange.Location,
|
||||||
|
): Promise<AdsbExchange.Aircraft[]> {
|
||||||
|
this.logger.verbose(
|
||||||
|
`Requesting aircraft within ${radius} nautical miles of ${location.lat}, ${location.long}`,
|
||||||
|
);
|
||||||
|
const options = {
|
||||||
|
method: 'GET',
|
||||||
|
url: `https://adsbexchange-com1.p.rapidapi.com/v2/lat/${location.lat}/lon/${location.long}/dist/${radius}/`,
|
||||||
|
headers: {
|
||||||
|
'x-rapidapi-key': this.configService.get<string>('rapidApiKey'),
|
||||||
|
'x-rapidapi-host': 'adsbexchange-com1.p.rapidapi.com',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response =
|
||||||
|
await axios.request<AdsbExchange.AdsbExchangeRadiusResponse>(options);
|
||||||
|
return response.data.ac;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extendAircraft(
|
||||||
|
aircraft: AdsbExchange.Aircraft[],
|
||||||
|
location: AdsbExchange.Location,
|
||||||
|
): ExtendedAircraft[] {
|
||||||
|
return aircraft.map((a) => ({
|
||||||
|
...a,
|
||||||
|
bearing: this.bearingToAircraft(a, location),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
liteAircraft = (aircraft: ExtendedAircraft[]) =>
|
||||||
|
aircraft.map((a) => ({
|
||||||
|
flight: a.flight,
|
||||||
|
registration: a.r,
|
||||||
|
type: a.t,
|
||||||
|
altitude: a.alt_baro,
|
||||||
|
speed: a.gs,
|
||||||
|
track: a.track,
|
||||||
|
bearing: a.bearing,
|
||||||
|
}));
|
||||||
|
}
|
189
src/adsb-exchange/types.ts
Normal file
189
src/adsb-exchange/types.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
export namespace AdsbExchange {
|
||||||
|
export const AircraftMessageTypes = [
|
||||||
|
'adsb_icao',
|
||||||
|
'adsb_icao_nt',
|
||||||
|
'adsr_icao',
|
||||||
|
'tisb_icao',
|
||||||
|
'adsc',
|
||||||
|
'mlat',
|
||||||
|
'other',
|
||||||
|
'mode_s',
|
||||||
|
'adsb_other',
|
||||||
|
'adsr_other',
|
||||||
|
'tisb_other',
|
||||||
|
'tisb_trackfile',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type AircraftMessageType = (typeof AircraftMessageTypes)[number];
|
||||||
|
|
||||||
|
export interface Location {
|
||||||
|
lat: number;
|
||||||
|
long: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Aircraft {
|
||||||
|
/**
|
||||||
|
* The 24-bit ICAO identifier of the aircraft
|
||||||
|
*/
|
||||||
|
hex: string;
|
||||||
|
/**
|
||||||
|
* Type of underlying message/best source of data
|
||||||
|
*/
|
||||||
|
type: AircraftMessageType | string;
|
||||||
|
/**
|
||||||
|
* Flight callsign or aircraft registration as 8 characters
|
||||||
|
*/
|
||||||
|
flight: string;
|
||||||
|
/**
|
||||||
|
* Aircraft registration pulled from database
|
||||||
|
*/
|
||||||
|
r: string;
|
||||||
|
/**
|
||||||
|
* Aircraft type pulled from database
|
||||||
|
*/
|
||||||
|
t: string;
|
||||||
|
/**
|
||||||
|
* Barometric altitude in feet or "ground"
|
||||||
|
*/
|
||||||
|
alt_baro: number | 'ground';
|
||||||
|
/**
|
||||||
|
* Geometric (GNSS/INS) altitude in feet
|
||||||
|
*/
|
||||||
|
alt_geom: number;
|
||||||
|
/**
|
||||||
|
* Groundspeed in knots
|
||||||
|
*/
|
||||||
|
gs: number;
|
||||||
|
/**
|
||||||
|
* True track over ground in degrees
|
||||||
|
*/
|
||||||
|
track: number;
|
||||||
|
/**
|
||||||
|
* Rate of change of geometric altitude in feet per minute
|
||||||
|
*/
|
||||||
|
geom_rate: number;
|
||||||
|
/**
|
||||||
|
* Mode A squawk code encoded as 4 octal digits
|
||||||
|
*/
|
||||||
|
squawk: string;
|
||||||
|
/**
|
||||||
|
* Emergency status
|
||||||
|
*/
|
||||||
|
emergency: string;
|
||||||
|
/**
|
||||||
|
* Emitter category to identify particular aircraft
|
||||||
|
* or vehicle classes. https://www.adsbexchange.com/emitter-category-ads-b-do-260b-2-2-3-2-5-2/
|
||||||
|
*/
|
||||||
|
category: string;
|
||||||
|
/**
|
||||||
|
* Aircraft location in decimal degrees
|
||||||
|
*/
|
||||||
|
lat: number;
|
||||||
|
/**
|
||||||
|
* Aircraft location in decimal degrees
|
||||||
|
*/
|
||||||
|
lon: number;
|
||||||
|
/**
|
||||||
|
* Navigation integrity category
|
||||||
|
*/
|
||||||
|
nic: number;
|
||||||
|
/**
|
||||||
|
* Radius of containment in meters
|
||||||
|
*/
|
||||||
|
rc: number;
|
||||||
|
/**
|
||||||
|
* How long ago in seconds the aircraft was last seen
|
||||||
|
*/
|
||||||
|
seen_pos: number;
|
||||||
|
/**
|
||||||
|
* ADS-B version
|
||||||
|
*/
|
||||||
|
version: number;
|
||||||
|
/**
|
||||||
|
* Navigation integrity category for barometric altitude
|
||||||
|
*/
|
||||||
|
nic_baro: number;
|
||||||
|
/**
|
||||||
|
* Navigation accuracy for position
|
||||||
|
*/
|
||||||
|
nac_p: number;
|
||||||
|
/**
|
||||||
|
* Navigation accuracy for velocity
|
||||||
|
*/
|
||||||
|
nac_v: number;
|
||||||
|
/**
|
||||||
|
* Source integrity level
|
||||||
|
*/
|
||||||
|
sil: number;
|
||||||
|
/**
|
||||||
|
* Interpretation of SIL
|
||||||
|
*/
|
||||||
|
sil_type: 'unknown' | 'perhour' | 'persample';
|
||||||
|
/**
|
||||||
|
* Geometric vertical accuracy
|
||||||
|
*/
|
||||||
|
gva: number;
|
||||||
|
/**
|
||||||
|
* System design assurance level
|
||||||
|
*/
|
||||||
|
sda: number;
|
||||||
|
/**
|
||||||
|
* Flight status alert bit
|
||||||
|
*/
|
||||||
|
alert: number;
|
||||||
|
/**
|
||||||
|
* Flight status special position indicator
|
||||||
|
*/
|
||||||
|
spi: number;
|
||||||
|
mlat: [];
|
||||||
|
tisb: [];
|
||||||
|
/**
|
||||||
|
* Total number of mode S messages received
|
||||||
|
* for this aircraft
|
||||||
|
*/
|
||||||
|
messages: number;
|
||||||
|
/**
|
||||||
|
* How long ago in seconds a message was last received
|
||||||
|
* for this aircraft
|
||||||
|
*/
|
||||||
|
seen: number;
|
||||||
|
/**
|
||||||
|
* Recent average signal strength in dbFS
|
||||||
|
*/
|
||||||
|
rssi: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdsbExchangeRadiusResponse {
|
||||||
|
ac: Aircraft[];
|
||||||
|
msg: string;
|
||||||
|
now: number;
|
||||||
|
total: number;
|
||||||
|
ctime: number;
|
||||||
|
ptime: number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Bearing {
|
||||||
|
/**
|
||||||
|
* Bearing in degrees
|
||||||
|
*/
|
||||||
|
bearing: number;
|
||||||
|
/**
|
||||||
|
* Distance in miles
|
||||||
|
*/
|
||||||
|
distance: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LiteAircraft {
|
||||||
|
flight?: string;
|
||||||
|
registration?: string;
|
||||||
|
type: string;
|
||||||
|
altitude: number | 'ground';
|
||||||
|
speed: number;
|
||||||
|
track: number;
|
||||||
|
bearing: Bearing;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtendedAircraft = AdsbExchange.Aircraft & {
|
||||||
|
bearing: Bearing;
|
||||||
|
};
|
@@ -8,7 +8,7 @@ import { UsersModule } from './users/users.module';
|
|||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { DomainrproxyModule } from './domainrproxy/domainrproxy.module';
|
import { DomainrproxyModule } from './domainrproxy/domainrproxy.module';
|
||||||
import configuration from './config/configuration';
|
import configuration from './config/configuration';
|
||||||
import { CacheModule } from '@nestjs/cache-manager';
|
import { CacheInterceptor, CacheModule } from '@nestjs/cache-manager';
|
||||||
import { IrcbotModule } from './ircbot/ircbot.module';
|
import { IrcbotModule } from './ircbot/ircbot.module';
|
||||||
import { OgScraperModule } from './ogscraper/ogscraper.module';
|
import { OgScraperModule } from './ogscraper/ogscraper.module';
|
||||||
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
|
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
|
||||||
@@ -25,6 +25,8 @@ import { PowModule } from './pow/pow.module';
|
|||||||
import { FocoCoffeeModule } from './fococoffee/fococoffee.module';
|
import { FocoCoffeeModule } from './fococoffee/fococoffee.module';
|
||||||
import { JobsModule } from './jobs/jobs.module';
|
import { JobsModule } from './jobs/jobs.module';
|
||||||
import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
|
import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
|
||||||
|
import { AdsbExchangeModule } from './adsb-exchange/adsb-exchange.module';
|
||||||
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -97,8 +99,15 @@ import { RedisModule, RedisModuleOptions } from '@liaoliaots/nestjs-redis';
|
|||||||
PowModule,
|
PowModule,
|
||||||
FocoCoffeeModule,
|
FocoCoffeeModule,
|
||||||
JobsModule,
|
JobsModule,
|
||||||
|
AdsbExchangeModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [
|
||||||
|
AppService,
|
||||||
|
{
|
||||||
|
provide: APP_INTERCEPTOR,
|
||||||
|
useClass: CacheInterceptor,
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
@@ -3326,6 +3326,11 @@ has@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
|
haversine-ts@^1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/haversine-ts/-/haversine-ts-1.2.0.tgz#4a9f3c7e4327abaf2c099d4aa7b35e909d1025cb"
|
||||||
|
integrity sha512-30SeQ+x9kbplPy5+4o2Sh8zc2eHzOepDbXnDLvkaLXmUT8SknAEAh/fqOqdpTDrH4SmUKFk1VBjgEyWNxBod/w==
|
||||||
|
|
||||||
hbs@^4.2.0:
|
hbs@^4.2.0:
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/hbs/-/hbs-4.2.0.tgz#10e40dcc24d5be7342df9636316896617542a32b"
|
resolved "https://registry.yarnpkg.com/hbs/-/hbs-4.2.0.tgz#10e40dcc24d5be7342df9636316896617542a32b"
|
||||||
|
Reference in New Issue
Block a user