@squawk/airspace 0.2.2 → 0.3.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 +36 -13
- package/dist/resolver.d.ts +35 -10
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +51 -57
- package/package.json +6 -4
- package/dist/point-in-polygon.d.ts +0 -15
- package/dist/point-in-polygon.d.ts.map +0 -1
- package/dist/point-in-polygon.js +0 -27
package/README.md
CHANGED
|
@@ -19,14 +19,17 @@ Part of the [@squawk](https://www.npmjs.com/org/squawk) aviation library suite.
|
|
|
19
19
|
import { usBundledAirspace } from '@squawk/airspace-data';
|
|
20
20
|
import { createAirspaceResolver } from '@squawk/airspace';
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const resolver = createAirspaceResolver({ data: usBundledAirspace });
|
|
23
23
|
|
|
24
24
|
// Query a position and altitude
|
|
25
|
-
const
|
|
25
|
+
const overhead = resolver.query({ lat: 33.9425, lon: -118.4081, altitudeFt: 3000 });
|
|
26
26
|
|
|
27
|
-
for (const f of
|
|
27
|
+
for (const f of overhead) {
|
|
28
28
|
console.log(f.type, f.name, f.identifier);
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
// Get every shell associated with an airport (for drawing the full wedding cake)
|
|
32
|
+
const laxShells = resolver.byAirport('LAX');
|
|
30
33
|
```
|
|
31
34
|
|
|
32
35
|
Consumers who have their own GeoJSON airspace data can use this package standalone:
|
|
@@ -34,19 +37,23 @@ Consumers who have their own GeoJSON airspace data can use this package standalo
|
|
|
34
37
|
```typescript
|
|
35
38
|
import { createAirspaceResolver } from '@squawk/airspace';
|
|
36
39
|
|
|
37
|
-
const
|
|
40
|
+
const resolver = createAirspaceResolver({ data: myGeoJson });
|
|
38
41
|
```
|
|
39
42
|
|
|
40
43
|
## How it works
|
|
41
44
|
|
|
42
45
|
`createAirspaceResolver` parses the GeoJSON FeatureCollection at initialization and
|
|
43
|
-
returns a resolver
|
|
44
|
-
feature:
|
|
46
|
+
returns a resolver object with two methods:
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
- `query(AirspaceQuery)` - returns features containing the given position and
|
|
49
|
+
altitude, via a ray casting point-in-polygon test combined with a vertical
|
|
50
|
+
floor/ceiling comparison.
|
|
51
|
+
- `byAirport(identifier, types?)` - returns every feature whose `identifier`
|
|
52
|
+
matches (case-insensitive). For Class B/C/D/E2 this groups all sectors of the
|
|
53
|
+
airspace around a given airport regardless of the point of interest.
|
|
48
54
|
|
|
49
|
-
All matching features are returned as `AirspaceFeature` objects (from `@squawk/types`)
|
|
55
|
+
All matching features are returned as `AirspaceFeature` objects (from `@squawk/types`),
|
|
56
|
+
including the full polygon boundary coordinates.
|
|
50
57
|
|
|
51
58
|
## AGL altitude handling
|
|
52
59
|
|
|
@@ -74,14 +81,14 @@ for (const f of features) {
|
|
|
74
81
|
|
|
75
82
|
### `createAirspaceResolver(options)`
|
|
76
83
|
|
|
77
|
-
Creates a resolver
|
|
84
|
+
Creates a resolver from a GeoJSON dataset.
|
|
78
85
|
|
|
79
86
|
**Parameters:**
|
|
80
87
|
|
|
81
88
|
- `options.data` - a GeoJSON `FeatureCollection` with airspace features
|
|
82
89
|
|
|
83
|
-
**Returns:** `AirspaceResolver` -
|
|
84
|
-
`(
|
|
90
|
+
**Returns:** `AirspaceResolver` - an object exposing `query(AirspaceQuery)` and
|
|
91
|
+
`byAirport(identifier, types?)` methods.
|
|
85
92
|
|
|
86
93
|
### `AirspaceQuery`
|
|
87
94
|
|
|
@@ -94,10 +101,26 @@ Creates a resolver function from a GeoJSON dataset.
|
|
|
94
101
|
|
|
95
102
|
```typescript
|
|
96
103
|
// Only query tower-controlled airspace (exclude Class E and SUA)
|
|
97
|
-
const controlled =
|
|
104
|
+
const controlled = resolver.query({
|
|
98
105
|
lat: 33.9425,
|
|
99
106
|
lon: -118.4081,
|
|
100
107
|
altitudeFt: 3000,
|
|
101
108
|
types: new Set(['CLASS_B', 'CLASS_C', 'CLASS_D']),
|
|
102
109
|
});
|
|
103
110
|
```
|
|
111
|
+
|
|
112
|
+
### `resolver.byAirport(identifier, types?)`
|
|
113
|
+
|
|
114
|
+
Returns every airspace feature whose `identifier` property matches. For
|
|
115
|
+
Class B/C/D/E2 this is the associated airport's FAA location identifier
|
|
116
|
+
(e.g. "JFK" for the NY Class B). For Special Use Airspace this is the NASR
|
|
117
|
+
designator (e.g. "R-2508"). Lookup is case-insensitive. ICAO-prefixed codes
|
|
118
|
+
like "KJFK" will not match - resolve to an FAA ID first via `@squawk/airports`.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Every sector of the NY Class B around JFK, with full polygon boundaries
|
|
122
|
+
const jfkShells = resolver.byAirport('JFK');
|
|
123
|
+
|
|
124
|
+
// Only the Class D for a towered field
|
|
125
|
+
const safClassD = resolver.byAirport('SAF', new Set(['CLASS_D']));
|
|
126
|
+
```
|
package/dist/resolver.d.ts
CHANGED
|
@@ -29,17 +29,41 @@ export interface AirspaceResolverOptions {
|
|
|
29
29
|
data: FeatureCollection;
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
* airspace features that contain that point laterally and vertically.
|
|
32
|
+
* Stateless resolver exposing airspace query methods.
|
|
34
33
|
*/
|
|
35
|
-
export
|
|
34
|
+
export interface AirspaceResolver {
|
|
35
|
+
/**
|
|
36
|
+
* Returns every airspace feature whose lateral polygon contains the given
|
|
37
|
+
* position and whose vertical bounds contain the given altitude.
|
|
38
|
+
*
|
|
39
|
+
* @param query - Position, altitude, and optional type filter.
|
|
40
|
+
* @returns All matching features, in no particular order.
|
|
41
|
+
*/
|
|
42
|
+
query(query: AirspaceQuery): AirspaceFeature[];
|
|
43
|
+
/**
|
|
44
|
+
* Returns every airspace feature associated with the given identifier,
|
|
45
|
+
* independent of position or altitude. Lookup is case-insensitive.
|
|
46
|
+
*
|
|
47
|
+
* For Class B/C/D/E2 airspace, the feature `identifier` is the associated
|
|
48
|
+
* airport's FAA location identifier (e.g. "JFK" for the NY Class B). For
|
|
49
|
+
* Special Use Airspace, it is the NASR designator (e.g. "R-2508"). Pass
|
|
50
|
+
* only the bare identifier - ICAO-prefixed codes like "KJFK" will not
|
|
51
|
+
* match; resolve to an FAA ID first via `@squawk/airports` if needed.
|
|
52
|
+
*
|
|
53
|
+
* @param identifier - FAA identifier or NASR designator.
|
|
54
|
+
* @param types - Optional type filter. Only features whose type is in this
|
|
55
|
+
* set are returned. When omitted, all types are returned.
|
|
56
|
+
* @returns All features whose identifier matches, or an empty array.
|
|
57
|
+
*/
|
|
58
|
+
byAirport(identifier: string, types?: ReadonlySet<AirspaceType>): AirspaceFeature[];
|
|
59
|
+
}
|
|
36
60
|
/**
|
|
37
|
-
* Creates a stateless airspace resolver
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
61
|
+
* Creates a stateless airspace resolver. The resolver accepts a GeoJSON
|
|
62
|
+
* FeatureCollection at initialization (typically from `@squawk/airspace-data`)
|
|
63
|
+
* and returns an object with methods for querying by position and altitude
|
|
64
|
+
* or by associated airport / SUA identifier.
|
|
41
65
|
*
|
|
42
|
-
*
|
|
66
|
+
* Position queries perform two checks per feature:
|
|
43
67
|
* 1. **Lateral** - point-in-polygon test against the feature boundary
|
|
44
68
|
* 2. **Vertical** - altitude comparison against floor/ceiling bounds
|
|
45
69
|
*
|
|
@@ -54,8 +78,9 @@ export type AirspaceResolver = (query: AirspaceQuery) => AirspaceFeature[];
|
|
|
54
78
|
* import { usBundledAirspace } from '@squawk/airspace-data';
|
|
55
79
|
* import { createAirspaceResolver } from '@squawk/airspace';
|
|
56
80
|
*
|
|
57
|
-
* const
|
|
58
|
-
* const
|
|
81
|
+
* const resolver = createAirspaceResolver({ data: usBundledAirspace });
|
|
82
|
+
* const overhead = resolver.query({ lat: 33.9425, lon: -118.4081, altitudeFt: 3000 });
|
|
83
|
+
* const laxShells = resolver.byAirport('LAX');
|
|
59
84
|
* ```
|
|
60
85
|
*/
|
|
61
86
|
export declare function createAirspaceResolver(options: AirspaceResolverOptions): AirspaceResolver;
|
package/dist/resolver.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../src/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAW,MAAM,SAAS,CAAC;AAC1D,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;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,KAAK,CAAC,KAAK,EAAE,aAAa,GAAG,eAAe,EAAE,CAAC;IAE/C;;;;;;;;;;;;;;OAcG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,WAAW,CAAC,YAAY,CAAC,GAAG,eAAe,EAAE,CAAC;CACrF;AAmDD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CAuDzF"}
|
package/dist/resolver.js
CHANGED
|
@@ -1,31 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { polygon } from '@squawk/geo';
|
|
2
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
3
|
/**
|
|
30
4
|
* Parses a GeoJSON Feature into an IndexedFeature, extracting the
|
|
31
5
|
* AirspaceFeature properties and polygon ring. Returns null if the
|
|
@@ -36,8 +10,7 @@ function parseFeature(geoFeature) {
|
|
|
36
10
|
if (!geom || geom.type !== 'Polygon') {
|
|
37
11
|
return null;
|
|
38
12
|
}
|
|
39
|
-
const
|
|
40
|
-
const ring = polygon.coordinates[0];
|
|
13
|
+
const ring = geom.coordinates[0];
|
|
41
14
|
if (!ring || ring.length < 4) {
|
|
42
15
|
return null;
|
|
43
16
|
}
|
|
@@ -51,20 +24,20 @@ function parseFeature(geoFeature) {
|
|
|
51
24
|
identifier: props.identifier ?? '',
|
|
52
25
|
floor: props.floor,
|
|
53
26
|
ceiling: props.ceiling,
|
|
54
|
-
boundary:
|
|
27
|
+
boundary: geom,
|
|
55
28
|
state: props.state ?? null,
|
|
56
29
|
controllingFacility: props.controllingFacility ?? null,
|
|
57
30
|
scheduleDescription: props.scheduleDescription ?? null,
|
|
58
31
|
};
|
|
59
|
-
return { feature, ring, boundingBox:
|
|
32
|
+
return { feature, ring, boundingBox: polygon.boundingBox(ring) };
|
|
60
33
|
}
|
|
61
34
|
/**
|
|
62
|
-
* Creates a stateless airspace resolver
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
35
|
+
* Creates a stateless airspace resolver. The resolver accepts a GeoJSON
|
|
36
|
+
* FeatureCollection at initialization (typically from `@squawk/airspace-data`)
|
|
37
|
+
* and returns an object with methods for querying by position and altitude
|
|
38
|
+
* or by associated airport / SUA identifier.
|
|
66
39
|
*
|
|
67
|
-
*
|
|
40
|
+
* Position queries perform two checks per feature:
|
|
68
41
|
* 1. **Lateral** - point-in-polygon test against the feature boundary
|
|
69
42
|
* 2. **Vertical** - altitude comparison against floor/ceiling bounds
|
|
70
43
|
*
|
|
@@ -79,39 +52,60 @@ function parseFeature(geoFeature) {
|
|
|
79
52
|
* import { usBundledAirspace } from '@squawk/airspace-data';
|
|
80
53
|
* import { createAirspaceResolver } from '@squawk/airspace';
|
|
81
54
|
*
|
|
82
|
-
* const
|
|
83
|
-
* const
|
|
55
|
+
* const resolver = createAirspaceResolver({ data: usBundledAirspace });
|
|
56
|
+
* const overhead = resolver.query({ lat: 33.9425, lon: -118.4081, altitudeFt: 3000 });
|
|
57
|
+
* const laxShells = resolver.byAirport('LAX');
|
|
84
58
|
* ```
|
|
85
59
|
*/
|
|
86
60
|
export function createAirspaceResolver(options) {
|
|
87
61
|
const indexed = [];
|
|
62
|
+
const byIdentifierMap = new Map();
|
|
88
63
|
for (const geoFeature of options.data.features) {
|
|
89
64
|
const parsed = parseFeature(geoFeature);
|
|
90
65
|
if (parsed) {
|
|
91
66
|
indexed.push(parsed);
|
|
67
|
+
const key = parsed.feature.identifier.toUpperCase();
|
|
68
|
+
if (key.length > 0) {
|
|
69
|
+
const bucket = byIdentifierMap.get(key);
|
|
70
|
+
if (bucket === undefined) {
|
|
71
|
+
byIdentifierMap.set(key, [parsed.feature]);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
bucket.push(parsed.feature);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
92
77
|
}
|
|
93
78
|
}
|
|
94
|
-
return
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
79
|
+
return {
|
|
80
|
+
query(query) {
|
|
81
|
+
const results = [];
|
|
82
|
+
const { lon, lat, altitudeFt, types } = query;
|
|
83
|
+
for (const { feature, ring, boundingBox } of indexed) {
|
|
84
|
+
if (types && !types.has(feature.type)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!polygon.pointInBoundingBox(lon, lat, boundingBox)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (!polygon.pointInPolygon(lon, lat, ring)) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!altitudeMatches(altitudeFt, feature.floor, feature.ceiling)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
results.push(feature);
|
|
100
97
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
98
|
+
return results;
|
|
99
|
+
},
|
|
100
|
+
byAirport(identifier, types) {
|
|
101
|
+
const bucket = byIdentifierMap.get(identifier.toUpperCase());
|
|
102
|
+
if (bucket === undefined) {
|
|
103
|
+
return [];
|
|
106
104
|
}
|
|
107
|
-
if (
|
|
108
|
-
|
|
105
|
+
if (types === undefined) {
|
|
106
|
+
return bucket.slice();
|
|
109
107
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
results.push(feature);
|
|
114
|
-
}
|
|
115
|
-
return results;
|
|
108
|
+
return bucket.filter((f) => types.has(f.type));
|
|
109
|
+
},
|
|
116
110
|
};
|
|
117
111
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@squawk/airspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pure logic library for querying US airspace geometry by position and altitude",
|
|
6
6
|
"author": "Neil Cochran",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"types": "./dist/index.d.ts",
|
|
20
20
|
"exports": {
|
|
21
21
|
".": {
|
|
22
|
-
"
|
|
23
|
-
"
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
@@ -30,9 +30,11 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsc",
|
|
32
32
|
"test": "node --test 'dist/**/*.spec.js'",
|
|
33
|
-
"lint": "tsc --noEmit && eslint src"
|
|
33
|
+
"lint": "tsc --noEmit && eslint src",
|
|
34
|
+
"lint:pack": "publint && attw --pack . --profile esm-only"
|
|
34
35
|
},
|
|
35
36
|
"dependencies": {
|
|
37
|
+
"@squawk/geo": "*",
|
|
36
38
|
"@squawk/types": "*"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
@@ -1,15 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/point-in-polygon.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|