@squawk/airspace 0.2.3 → 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 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 resolve = createAirspaceResolver({ data: usBundledAirspace });
22
+ const resolver = createAirspaceResolver({ data: usBundledAirspace });
23
23
 
24
24
  // Query a position and altitude
25
- const features = resolve({ lat: 33.9425, lon: -118.4081, altitudeFt: 3000 });
25
+ const overhead = resolver.query({ lat: 33.9425, lon: -118.4081, altitudeFt: 3000 });
26
26
 
27
- for (const f of features) {
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 resolve = createAirspaceResolver({ data: myGeoJson });
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 function. Each call to the resolver performs two checks per
44
- feature:
46
+ returns a resolver object with two methods:
45
47
 
46
- 1. **Lateral** - a ray casting point-in-polygon test against the feature boundary
47
- 2. **Vertical** - altitude comparison against floor and ceiling bounds
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 function from a GeoJSON dataset.
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` - a function with signature
84
- `(query: AirspaceQuery) => AirspaceFeature[]`
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 = resolve({
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
+ ```
@@ -29,17 +29,41 @@ export interface AirspaceResolverOptions {
29
29
  data: FeatureCollection;
30
30
  }
31
31
  /**
32
- * A function that accepts a position and altitude and returns all
33
- * airspace features that contain that point laterally and vertically.
32
+ * Stateless resolver exposing airspace query methods.
34
33
  */
35
- export type AirspaceResolver = (query: AirspaceQuery) => AirspaceFeature[];
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 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.
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
- * The resolver performs two checks for each feature:
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 resolve = createAirspaceResolver({ data: usBundledAirspace });
58
- * const features = resolve({ lat: 33.9425, lon: -118.4081, altitudeFt: 3000 });
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;
@@ -1 +1 @@
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;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,eAAe,EAAE,CAAC;AAmD3E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,uBAAuB,GAAG,gBAAgB,CAgCzF"}
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
@@ -32,12 +32,12 @@ function parseFeature(geoFeature) {
32
32
  return { feature, ring, boundingBox: polygon.boundingBox(ring) };
33
33
  }
34
34
  /**
35
- * Creates a stateless airspace resolver function. The resolver accepts a
36
- * GeoJSON FeatureCollection at initialization (typically from
37
- * `@squawk/airspace-data`) and returns a function that, given a position
38
- * and altitude, returns all matching airspace features.
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.
39
39
  *
40
- * The resolver performs two checks for each feature:
40
+ * Position queries perform two checks per feature:
41
41
  * 1. **Lateral** - point-in-polygon test against the feature boundary
42
42
  * 2. **Vertical** - altitude comparison against floor/ceiling bounds
43
43
  *
@@ -52,36 +52,60 @@ function parseFeature(geoFeature) {
52
52
  * import { usBundledAirspace } from '@squawk/airspace-data';
53
53
  * import { createAirspaceResolver } from '@squawk/airspace';
54
54
  *
55
- * const resolve = createAirspaceResolver({ data: usBundledAirspace });
56
- * const features = resolve({ lat: 33.9425, lon: -118.4081, altitudeFt: 3000 });
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');
57
58
  * ```
58
59
  */
59
60
  export function createAirspaceResolver(options) {
60
61
  const indexed = [];
62
+ const byIdentifierMap = new Map();
61
63
  for (const geoFeature of options.data.features) {
62
64
  const parsed = parseFeature(geoFeature);
63
65
  if (parsed) {
64
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
+ }
65
77
  }
66
78
  }
67
- return (query) => {
68
- const results = [];
69
- const { lon, lat, altitudeFt, types } = query;
70
- for (const { feature, ring, boundingBox } of indexed) {
71
- if (types && !types.has(feature.type)) {
72
- continue;
73
- }
74
- if (!polygon.pointInBoundingBox(lon, lat, boundingBox)) {
75
- continue;
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);
76
97
  }
77
- if (!polygon.pointInPolygon(lon, lat, ring)) {
78
- continue;
98
+ return results;
99
+ },
100
+ byAirport(identifier, types) {
101
+ const bucket = byIdentifierMap.get(identifier.toUpperCase());
102
+ if (bucket === undefined) {
103
+ return [];
79
104
  }
80
- if (!altitudeMatches(altitudeFt, feature.floor, feature.ceiling)) {
81
- continue;
105
+ if (types === undefined) {
106
+ return bucket.slice();
82
107
  }
83
- results.push(feature);
84
- }
85
- return results;
108
+ return bucket.filter((f) => types.has(f.type));
109
+ },
86
110
  };
87
111
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squawk/airspace",
3
- "version": "0.2.3",
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
- "import": "./dist/index.js",
23
- "types": "./dist/index.d.ts"
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
24
  }
25
25
  },
26
26
  "files": [
@@ -30,7 +30,8 @@
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": {
36
37
  "@squawk/geo": "*",