@squawk/airspace 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/point-in-polygon.d.ts +15 -0
- package/dist/point-in-polygon.d.ts.map +1 -0
- package/dist/point-in-polygon.js +27 -0
- package/dist/resolver.d.ts +62 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +117 -0
- package/dist/vertical-filter.d.ts +23 -0
- package/dist/vertical-filter.d.ts.map +1 -0
- package/dist/vertical-filter.js +48 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# @squawk/airspace
|
|
2
|
+
|
|
3
|
+
Pure logic library for querying US airspace geometry. Given a position and altitude,
|
|
4
|
+
returns all applicable airspace designations. Contains no bundled data - accepts a
|
|
5
|
+
GeoJSON dataset at initialization. For zero-config use, pair with `@squawk/airspace-data`.
|
|
6
|
+
|
|
7
|
+
## Coverage
|
|
8
|
+
|
|
9
|
+
- Class B, C, D, and E controlled airspace (E2 through E7 subtypes)
|
|
10
|
+
- Special Use Airspace: MOAs, restricted, prohibited, warning, alert, and national security areas
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { usBundledAirspace } from '@squawk/airspace-data';
|
|
16
|
+
import { createAirspaceResolver } from '@squawk/airspace';
|
|
17
|
+
|
|
18
|
+
const resolve = createAirspaceResolver({ data: usBundledAirspace });
|
|
19
|
+
|
|
20
|
+
// Query a position and altitude
|
|
21
|
+
const features = resolve({ lat: 33.9425, lon: -118.4081, altitudeFt: 3000 });
|
|
22
|
+
|
|
23
|
+
for (const f of features) {
|
|
24
|
+
console.log(f.type, f.name, f.identifier);
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Consumers who have their own GeoJSON airspace data can use this package standalone:
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { createAirspaceResolver } from '@squawk/airspace';
|
|
32
|
+
|
|
33
|
+
const resolve = createAirspaceResolver({ data: myGeoJson });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
`createAirspaceResolver` parses the GeoJSON FeatureCollection at initialization and
|
|
39
|
+
returns a resolver function. Each call to the resolver performs two checks per
|
|
40
|
+
feature:
|
|
41
|
+
|
|
42
|
+
1. **Lateral** - a ray casting point-in-polygon test against the feature boundary
|
|
43
|
+
2. **Vertical** - altitude comparison against floor and ceiling bounds
|
|
44
|
+
|
|
45
|
+
All matching features are returned as `AirspaceFeature` objects (from `@squawk/types`).
|
|
46
|
+
|
|
47
|
+
## AGL altitude handling
|
|
48
|
+
|
|
49
|
+
Some airspace features have floor or ceiling bounds referenced to AGL (above ground
|
|
50
|
+
level) rather than MSL. Converting AGL to MSL requires terrain elevation data that
|
|
51
|
+
this library does not include.
|
|
52
|
+
|
|
53
|
+
The resolver handles AGL bounds conservatively: when it cannot determine the MSL
|
|
54
|
+
equivalent, it **includes** the feature rather than silently excluding it. This means
|
|
55
|
+
the resolver may return features whose AGL bounds do not actually contain the
|
|
56
|
+
queried altitude.
|
|
57
|
+
|
|
58
|
+
Consumers can inspect the `reference` field on the returned `AltitudeBound` objects
|
|
59
|
+
and apply their own terrain lookup if needed:
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
for (const f of features) {
|
|
63
|
+
if (f.floor.reference === 'AGL') {
|
|
64
|
+
// This feature's floor is AGL - apply terrain data if available
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## API
|
|
70
|
+
|
|
71
|
+
### `createAirspaceResolver(options)`
|
|
72
|
+
|
|
73
|
+
Creates a resolver function from a GeoJSON dataset.
|
|
74
|
+
|
|
75
|
+
**Parameters:**
|
|
76
|
+
|
|
77
|
+
- `options.data` - a GeoJSON `FeatureCollection` with airspace features
|
|
78
|
+
|
|
79
|
+
**Returns:** `AirspaceResolver` - a function with signature
|
|
80
|
+
`(query: AirspaceQuery) => AirspaceFeature[]`
|
|
81
|
+
|
|
82
|
+
### `AirspaceQuery`
|
|
83
|
+
|
|
84
|
+
| Property | Type | Description |
|
|
85
|
+
| ------------ | -------------------------- | ------------------------------------------------------------------ |
|
|
86
|
+
| `lat` | number | Latitude in decimal degrees (WGS84) |
|
|
87
|
+
| `lon` | number | Longitude in decimal degrees (WGS84) |
|
|
88
|
+
| `altitudeFt` | number | Altitude in feet MSL |
|
|
89
|
+
| `types` | ReadonlySet\<AirspaceType> | Optional. When provided, only features of these types are returned |
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// Only query tower-controlled airspace (exclude Class E and SUA)
|
|
93
|
+
const controlled = resolve({
|
|
94
|
+
lat: 33.9425,
|
|
95
|
+
lon: -118.4081,
|
|
96
|
+
altitudeFt: 3000,
|
|
97
|
+
types: new Set(['CLASS_B', 'CLASS_C', 'CLASS_D']),
|
|
98
|
+
});
|
|
99
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,YAAY,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createAirspaceResolver } from './resolver.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests whether a point is inside a polygon using the ray casting algorithm.
|
|
3
|
+
* The polygon is represented as an array of [lon, lat] coordinate pairs
|
|
4
|
+
* forming a closed ring (first and last points are identical).
|
|
5
|
+
*
|
|
6
|
+
* Returns true if the point is inside or on the boundary of the polygon.
|
|
7
|
+
*/
|
|
8
|
+
export declare function pointInPolygon(
|
|
9
|
+
/** Longitude of the test point in decimal degrees. */
|
|
10
|
+
x: number,
|
|
11
|
+
/** Latitude of the test point in decimal degrees. */
|
|
12
|
+
y: number,
|
|
13
|
+
/** Polygon exterior ring as [lon, lat] coordinate pairs. */
|
|
14
|
+
ring: number[][]): boolean;
|
|
15
|
+
//# sourceMappingURL=point-in-polygon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"point-in-polygon.d.ts","sourceRoot":"","sources":["../src/point-in-polygon.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,cAAc;AAC5B,sDAAsD;AACtD,CAAC,EAAE,MAAM;AACT,qDAAqD;AACrD,CAAC,EAAE,MAAM;AACT,4DAA4D;AAC5D,IAAI,EAAE,MAAM,EAAE,EAAE,GACf,OAAO,CAgBT"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests whether a point is inside a polygon using the ray casting algorithm.
|
|
3
|
+
* The polygon is represented as an array of [lon, lat] coordinate pairs
|
|
4
|
+
* forming a closed ring (first and last points are identical).
|
|
5
|
+
*
|
|
6
|
+
* Returns true if the point is inside or on the boundary of the polygon.
|
|
7
|
+
*/
|
|
8
|
+
export function pointInPolygon(
|
|
9
|
+
/** Longitude of the test point in decimal degrees. */
|
|
10
|
+
x,
|
|
11
|
+
/** Latitude of the test point in decimal degrees. */
|
|
12
|
+
y,
|
|
13
|
+
/** Polygon exterior ring as [lon, lat] coordinate pairs. */
|
|
14
|
+
ring) {
|
|
15
|
+
let inside = false;
|
|
16
|
+
const len = ring.length;
|
|
17
|
+
for (let i = 0, j = len - 1; i < len; j = i++) {
|
|
18
|
+
const xi = ring[i][0];
|
|
19
|
+
const yi = ring[i][1];
|
|
20
|
+
const xj = ring[j][0];
|
|
21
|
+
const yj = ring[j][1];
|
|
22
|
+
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) {
|
|
23
|
+
inside = !inside;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return inside;
|
|
27
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { FeatureCollection } from 'geojson';
|
|
2
|
+
import type { AirspaceFeature, AirspaceType } from '@squawk/types';
|
|
3
|
+
/**
|
|
4
|
+
* A query describing a geographic position and altitude to resolve
|
|
5
|
+
* against loaded airspace data.
|
|
6
|
+
*/
|
|
7
|
+
export interface AirspaceQuery {
|
|
8
|
+
/** Latitude in decimal degrees (WGS84). */
|
|
9
|
+
lat: number;
|
|
10
|
+
/** Longitude in decimal degrees (WGS84). */
|
|
11
|
+
lon: number;
|
|
12
|
+
/** Altitude in feet MSL to compare against airspace vertical bounds. */
|
|
13
|
+
altitudeFt: number;
|
|
14
|
+
/**
|
|
15
|
+
* Optional set of airspace types to include in the results. When provided,
|
|
16
|
+
* only features whose type is in this set are considered. Features of other
|
|
17
|
+
* types are skipped before any geometry or altitude checks, improving query
|
|
18
|
+
* performance when only specific airspace classes are needed.
|
|
19
|
+
*
|
|
20
|
+
* When omitted, all airspace types are included.
|
|
21
|
+
*/
|
|
22
|
+
types?: ReadonlySet<AirspaceType>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Options for creating an airspace resolver.
|
|
26
|
+
*/
|
|
27
|
+
export interface AirspaceResolverOptions {
|
|
28
|
+
/** GeoJSON FeatureCollection containing airspace features. */
|
|
29
|
+
data: FeatureCollection;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A function that accepts a position and altitude and returns all
|
|
33
|
+
* airspace features that contain that point laterally and vertically.
|
|
34
|
+
*/
|
|
35
|
+
export type AirspaceResolver = (query: AirspaceQuery) => AirspaceFeature[];
|
|
36
|
+
/**
|
|
37
|
+
* Creates a stateless airspace resolver function. The resolver accepts a
|
|
38
|
+
* GeoJSON FeatureCollection at initialization (typically from
|
|
39
|
+
* `@squawk/airspace-data`) and returns a function that, given a position
|
|
40
|
+
* and altitude, returns all matching airspace features.
|
|
41
|
+
*
|
|
42
|
+
* The resolver performs two checks for each feature:
|
|
43
|
+
* 1. **Lateral** - point-in-polygon test against the feature boundary
|
|
44
|
+
* 2. **Vertical** - altitude comparison against floor/ceiling bounds
|
|
45
|
+
*
|
|
46
|
+
* AGL-referenced altitude bounds are handled conservatively: when the
|
|
47
|
+
* resolver cannot determine the MSL equivalent (because it has no terrain
|
|
48
|
+
* data), it includes the feature rather than silently excluding it. This
|
|
49
|
+
* means the resolver may return features whose AGL bounds do not actually
|
|
50
|
+
* contain the queried altitude. Consumers can inspect the returned
|
|
51
|
+
* AltitudeBound references and apply their own terrain lookup if needed.
|
|
52
|
+
*
|
|
53
|
+
* ```typescript
|
|
54
|
+
* import { usBundledAirspace } from '@squawk/airspace-data';
|
|
55
|
+
* import { createAirspaceResolver } from '@squawk/airspace';
|
|
56
|
+
*
|
|
57
|
+
* const resolve = createAirspaceResolver({ data: usBundledAirspace });
|
|
58
|
+
* const features = resolve({ lat: 33.9425, lon: -118.4081, altitudeFt: 3000 });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function createAirspaceResolver(options: AirspaceResolverOptions): AirspaceResolver;
|
|
62
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAiB,MAAM,eAAe,CAAC;AAIlF;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,wEAAwE;IACxE,UAAU,EAAE,MAAM,CAAC;IACnB;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,8DAA8D;IAC9D,IAAI,EAAE,iBAAiB,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,eAAe,EAAE,CAAC;AA+F3E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CAqCzF"}
|
package/dist/resolver.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { pointInPolygon } from './point-in-polygon.js';
|
|
2
|
+
import { altitudeMatches } from './vertical-filter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Computes an axis-aligned bounding box from a polygon exterior ring.
|
|
5
|
+
*/
|
|
6
|
+
function computeBoundingBox(ring) {
|
|
7
|
+
let minLon = Infinity;
|
|
8
|
+
let maxLon = -Infinity;
|
|
9
|
+
let minLat = Infinity;
|
|
10
|
+
let maxLat = -Infinity;
|
|
11
|
+
for (const coord of ring) {
|
|
12
|
+
const lon = coord[0];
|
|
13
|
+
const lat = coord[1];
|
|
14
|
+
if (lon < minLon) {
|
|
15
|
+
minLon = lon;
|
|
16
|
+
}
|
|
17
|
+
if (lon > maxLon) {
|
|
18
|
+
maxLon = lon;
|
|
19
|
+
}
|
|
20
|
+
if (lat < minLat) {
|
|
21
|
+
minLat = lat;
|
|
22
|
+
}
|
|
23
|
+
if (lat > maxLat) {
|
|
24
|
+
maxLat = lat;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { minLon, maxLon, minLat, maxLat };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parses a GeoJSON Feature into an IndexedFeature, extracting the
|
|
31
|
+
* AirspaceFeature properties and polygon ring. Returns null if the
|
|
32
|
+
* feature cannot be parsed (missing geometry, invalid type, etc.).
|
|
33
|
+
*/
|
|
34
|
+
function parseFeature(geoFeature) {
|
|
35
|
+
const geom = geoFeature.geometry;
|
|
36
|
+
if (!geom || geom.type !== 'Polygon') {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const polygon = geom;
|
|
40
|
+
const ring = polygon.coordinates[0];
|
|
41
|
+
if (!ring || ring.length < 4) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const props = geoFeature.properties;
|
|
45
|
+
if (!props) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const feature = {
|
|
49
|
+
type: props.type,
|
|
50
|
+
name: props.name ?? '',
|
|
51
|
+
identifier: props.identifier ?? '',
|
|
52
|
+
floor: props.floor,
|
|
53
|
+
ceiling: props.ceiling,
|
|
54
|
+
boundary: polygon,
|
|
55
|
+
state: props.state ?? null,
|
|
56
|
+
controllingFacility: props.controllingFacility ?? null,
|
|
57
|
+
scheduleDescription: props.scheduleDescription ?? null,
|
|
58
|
+
};
|
|
59
|
+
return { feature, ring, boundingBox: computeBoundingBox(ring) };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Creates a stateless airspace resolver function. The resolver accepts a
|
|
63
|
+
* GeoJSON FeatureCollection at initialization (typically from
|
|
64
|
+
* `@squawk/airspace-data`) and returns a function that, given a position
|
|
65
|
+
* and altitude, returns all matching airspace features.
|
|
66
|
+
*
|
|
67
|
+
* The resolver performs two checks for each feature:
|
|
68
|
+
* 1. **Lateral** - point-in-polygon test against the feature boundary
|
|
69
|
+
* 2. **Vertical** - altitude comparison against floor/ceiling bounds
|
|
70
|
+
*
|
|
71
|
+
* AGL-referenced altitude bounds are handled conservatively: when the
|
|
72
|
+
* resolver cannot determine the MSL equivalent (because it has no terrain
|
|
73
|
+
* data), it includes the feature rather than silently excluding it. This
|
|
74
|
+
* means the resolver may return features whose AGL bounds do not actually
|
|
75
|
+
* contain the queried altitude. Consumers can inspect the returned
|
|
76
|
+
* AltitudeBound references and apply their own terrain lookup if needed.
|
|
77
|
+
*
|
|
78
|
+
* ```typescript
|
|
79
|
+
* import { usBundledAirspace } from '@squawk/airspace-data';
|
|
80
|
+
* import { createAirspaceResolver } from '@squawk/airspace';
|
|
81
|
+
*
|
|
82
|
+
* const resolve = createAirspaceResolver({ data: usBundledAirspace });
|
|
83
|
+
* const features = resolve({ lat: 33.9425, lon: -118.4081, altitudeFt: 3000 });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export function createAirspaceResolver(options) {
|
|
87
|
+
const indexed = [];
|
|
88
|
+
for (const geoFeature of options.data.features) {
|
|
89
|
+
const parsed = parseFeature(geoFeature);
|
|
90
|
+
if (parsed) {
|
|
91
|
+
indexed.push(parsed);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return (query) => {
|
|
95
|
+
const results = [];
|
|
96
|
+
const { lon, lat, altitudeFt, types } = query;
|
|
97
|
+
for (const { feature, ring, boundingBox } of indexed) {
|
|
98
|
+
if (types && !types.has(feature.type)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (lon < boundingBox.minLon ||
|
|
102
|
+
lon > boundingBox.maxLon ||
|
|
103
|
+
lat < boundingBox.minLat ||
|
|
104
|
+
lat > boundingBox.maxLat) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!pointInPolygon(lon, lat, ring)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!altitudeMatches(altitudeFt, feature.floor, feature.ceiling)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
results.push(feature);
|
|
114
|
+
}
|
|
115
|
+
return results;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AltitudeBound } from '@squawk/types';
|
|
2
|
+
/**
|
|
3
|
+
* Tests whether an altitude in feet MSL falls within the vertical bounds
|
|
4
|
+
* defined by a floor and ceiling AltitudeBound pair.
|
|
5
|
+
*
|
|
6
|
+
* Altitude reference handling:
|
|
7
|
+
* - `SFC` - treated as 0 ft MSL (surface level)
|
|
8
|
+
* - `MSL` - compared directly against the queried altitude
|
|
9
|
+
* - `AGL` - the vertical check is skipped for that bound (returns true),
|
|
10
|
+
* because converting AGL to MSL requires terrain elevation data that this
|
|
11
|
+
* library does not have. This is a conservative approach: the feature is
|
|
12
|
+
* included rather than silently excluded when the bound cannot be resolved.
|
|
13
|
+
* Consumers can inspect the returned AltitudeBound references and apply
|
|
14
|
+
* their own terrain lookup if needed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function altitudeMatches(
|
|
17
|
+
/** Queried altitude in feet MSL. */
|
|
18
|
+
altitudeFt: number,
|
|
19
|
+
/** Lower vertical bound of the airspace feature. */
|
|
20
|
+
floor: AltitudeBound,
|
|
21
|
+
/** Upper vertical bound of the airspace feature. */
|
|
22
|
+
ceiling: AltitudeBound): boolean;
|
|
23
|
+
//# sourceMappingURL=vertical-filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vertical-filter.d.ts","sourceRoot":"","sources":["../src/vertical-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe;AAC7B,oCAAoC;AACpC,UAAU,EAAE,MAAM;AAClB,oDAAoD;AACpD,KAAK,EAAE,aAAa;AACpB,oDAAoD;AACpD,OAAO,EAAE,aAAa,GACrB,OAAO,CAaT"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests whether an altitude in feet MSL falls within the vertical bounds
|
|
3
|
+
* defined by a floor and ceiling AltitudeBound pair.
|
|
4
|
+
*
|
|
5
|
+
* Altitude reference handling:
|
|
6
|
+
* - `SFC` - treated as 0 ft MSL (surface level)
|
|
7
|
+
* - `MSL` - compared directly against the queried altitude
|
|
8
|
+
* - `AGL` - the vertical check is skipped for that bound (returns true),
|
|
9
|
+
* because converting AGL to MSL requires terrain elevation data that this
|
|
10
|
+
* library does not have. This is a conservative approach: the feature is
|
|
11
|
+
* included rather than silently excluded when the bound cannot be resolved.
|
|
12
|
+
* Consumers can inspect the returned AltitudeBound references and apply
|
|
13
|
+
* their own terrain lookup if needed.
|
|
14
|
+
*/
|
|
15
|
+
export function altitudeMatches(
|
|
16
|
+
/** Queried altitude in feet MSL. */
|
|
17
|
+
altitudeFt,
|
|
18
|
+
/** Lower vertical bound of the airspace feature. */
|
|
19
|
+
floor,
|
|
20
|
+
/** Upper vertical bound of the airspace feature. */
|
|
21
|
+
ceiling) {
|
|
22
|
+
const floorFt = resolveAltitude(floor);
|
|
23
|
+
const ceilingFt = resolveAltitude(ceiling);
|
|
24
|
+
// If either bound is AGL (resolved as null), skip that side of the check.
|
|
25
|
+
if (floorFt !== null && altitudeFt < floorFt) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (ceilingFt !== null && altitudeFt > ceilingFt) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolves an AltitudeBound to a feet MSL value for comparison, or null
|
|
35
|
+
* if the bound uses AGL reference and cannot be resolved without terrain data.
|
|
36
|
+
*/
|
|
37
|
+
function resolveAltitude(bound) {
|
|
38
|
+
switch (bound.reference) {
|
|
39
|
+
case 'SFC':
|
|
40
|
+
return 0;
|
|
41
|
+
case 'MSL':
|
|
42
|
+
return bound.valueFt;
|
|
43
|
+
case 'AGL':
|
|
44
|
+
return null;
|
|
45
|
+
default:
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@squawk/airspace",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Pure logic library for querying US airspace geometry by position and altitude",
|
|
6
|
+
"author": "Neil Cochran",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/neilcochran/squawk",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/neilcochran/squawk.git",
|
|
12
|
+
"directory": "packages/airspace"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=22"
|
|
16
|
+
},
|
|
17
|
+
"typedocMain": "src/index.ts",
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"!dist/**/*.spec.*"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"test": "node --test 'dist/**/*.spec.js'",
|
|
33
|
+
"lint": "tsc --noEmit && eslint src"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@squawk/types": "*"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@squawk/airspace-data": "*",
|
|
40
|
+
"@types/node": "^25.5.2"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"aviation",
|
|
44
|
+
"typescript",
|
|
45
|
+
"airspace",
|
|
46
|
+
"geojson",
|
|
47
|
+
"class-b",
|
|
48
|
+
"class-c",
|
|
49
|
+
"sua",
|
|
50
|
+
"faa",
|
|
51
|
+
"nasr"
|
|
52
|
+
],
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|