@chaosity/location-client 0.4.1 → 0.5.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.
@@ -2,6 +2,47 @@ import { GeocodeCommand, GetPlaceAdditionalFeature, GetPlaceCommand, ReverseGeoc
2
2
  import { geocodeResponseToFeatureCollection, getPlaceResponseToFeatureCollection, reverseGeocodeResponseToFeatureCollection, } from '@aws/amazon-location-utilities-datatypes';
3
3
  import debug from 'debug';
4
4
  const log = debug('location-client:geocoder');
5
+ /**
6
+ * Give the control the fields it renders, not just the ones GeoJSON needs.
7
+ *
8
+ * `@maplibre/maplibre-gl-geocoder` is a Carmen-shaped consumer: its result list
9
+ * calls `item.place_name.split(',')`, the input takes `result.place_name` on
10
+ * select, and fly-to reads `center` / `bbox`. The AWS converters produce plain
11
+ * GeoJSON — `properties` and `geometry` only — so a forward geocode used to
12
+ * hand the control features with no `place_name`. The list renderer threw, the
13
+ * control caught it and emitted `error`, and with no listener that surfaced as
14
+ * `Unhandled error. (undefined)` in the console: the Enter key silently did
15
+ * nothing. Suggestions were unaffected because that path renders `text`.
16
+ *
17
+ * The `as MaplibreGeocoderFeatureResults` cast this replaces is what hid it —
18
+ * `place_name` and `text` are required on the type.
19
+ */
20
+ function toCarmenFeatures(features) {
21
+ return features.map((feature) => {
22
+ const p = (feature.properties ?? {});
23
+ const title = typeof p.Title === 'string' ? p.Title : '';
24
+ const label = typeof p['Address.Label'] === 'string' ? p['Address.Label'] : '';
25
+ // `flattenProperties` flattens nested OBJECTS (Address.Label) but leaves an
26
+ // array of numbers intact, so MapView is still [minx, miny, maxx, maxy].
27
+ const view = p.MapView;
28
+ const bbox = Array.isArray(view) &&
29
+ view.length === 4 &&
30
+ view.every((v) => typeof v === 'number')
31
+ ? view
32
+ : undefined;
33
+ const center = feature.geometry?.type === 'Point'
34
+ ? feature.geometry.coordinates
35
+ : undefined;
36
+ return {
37
+ ...feature,
38
+ text: title || label,
39
+ place_name: label || title,
40
+ place_type: typeof p.PlaceType === 'string' ? [p.PlaceType] : [],
41
+ ...(bbox ? { bbox } : {}),
42
+ ...(center ? { center } : {}),
43
+ };
44
+ });
45
+ }
5
46
  export class GeoPlaces {
6
47
  constructor(client, map, options = {}) {
7
48
  this.client = client;
@@ -53,10 +94,14 @@ export class GeoPlaces {
53
94
  };
54
95
  }
55
96
  const response = (await this.client.send(new GeocodeCommand(commandInput)));
56
- const result = geocodeResponseToFeatureCollection(response, {
97
+ const converted = geocodeResponseToFeatureCollection(response, {
57
98
  flattenProperties: true,
58
99
  });
59
- log('forwardGeocode returned %d results', result.features?.length ?? 0);
100
+ const result = {
101
+ type: 'FeatureCollection',
102
+ features: toCarmenFeatures(converted.features),
103
+ };
104
+ log('forwardGeocode returned %d results', result.features.length);
60
105
  return result;
61
106
  }
62
107
  async reverseGeocode(config) {
@@ -70,10 +115,14 @@ export class GeoPlaces {
70
115
  Language: this.normalizeLanguage(config.language),
71
116
  };
72
117
  const response = (await this.client.send(new ReverseGeocodeCommand(commandInput)));
73
- const result = reverseGeocodeResponseToFeatureCollection(response, {
118
+ const converted = reverseGeocodeResponseToFeatureCollection(response, {
74
119
  flattenProperties: true,
75
120
  });
76
- log('reverseGeocode returned %d results', result.features?.length ?? 0);
121
+ const result = {
122
+ type: 'FeatureCollection',
123
+ features: toCarmenFeatures(converted.features),
124
+ };
125
+ log('reverseGeocode returned %d results', result.features.length);
77
126
  return result;
78
127
  }
79
128
  async getSuggestions(config) {
package/dist/index.d.ts CHANGED
@@ -15,6 +15,8 @@ export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/m
15
15
  export type { PoiCategory } from './maps/mapPoi';
16
16
  export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
17
17
  export type { MapStyleOptions } from './maps/mapStyle';
18
+ export { buildStaticMapUrl, fetchStaticMap, staticMapAccept, } from './maps/staticMap';
19
+ export type { StaticMapFileName, StaticMapOptions } from './maps/staticMap';
18
20
  export { BUILDINGS, COLOR_SCHEMES, CONTOUR_DENSITIES, LABEL_SIZES, MAP_FEATURE_MODES, MAP_STYLES, SCALE_BAR_UNITS, SPRITE_VARIANTS, STATIC_MAP_STYLES, TERRAINS, TRAFFIC_MODES, TRAVEL_MODES, } from './maps/mapEnums';
19
21
  export type { Buildings, ColorScheme, ContourDensity, LabelSize, MapFeatureMode, MapStyle, ScaleBarUnit, SpriteVariant, StaticMapStyle, Terrain, TrafficMode, TravelMode, } from './maps/mapEnums';
20
22
  export { transformRequest } from './maps/Utils';
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ export { createTransformRequest } from './maps/createTransformRequest';
17
17
  export { applyMapLanguage } from './maps/mapLanguage';
18
18
  export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi';
19
19
  export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
20
+ export { buildStaticMapUrl, fetchStaticMap, staticMapAccept, } from './maps/staticMap';
20
21
  // Accepted values for every map parameter, as VALUES so a picker can be built
21
22
  // from them, plus the matching types. Case sensitive — see mapEnums.ts.
22
23
  export { BUILDINGS, COLOR_SCHEMES, CONTOUR_DENSITIES, LABEL_SIZES, MAP_FEATURE_MODES, MAP_STYLES, SCALE_BAR_UNITS, SPRITE_VARIANTS, STATIC_MAP_STYLES, TERRAINS, TRAFFIC_MODES, TRAVEL_MODES, } from './maps/mapEnums';
@@ -0,0 +1,85 @@
1
+ import type { ColorScheme, LabelSize, MapFeatureMode, ScaleBarUnit, StaticMapStyle } from './mapEnums';
2
+ /**
3
+ * Static maps: build the URL, send the right headers, get a Blob.
4
+ *
5
+ * This exists because `/maps/static/{fileName}` is the one map route MapLibre
6
+ * never requests, so `createTransformRequest` never sees it and every caller was
7
+ * left to hand-roll the fetch. Ours did — `nextjs-address-finder-full`'s
8
+ * NearbySearch.tsx carried a comment explaining the rule — which is the clearest
9
+ * sign it belonged in the library (client#25, api#35).
10
+ *
11
+ * Two things make a plain fetch fail, and neither is discoverable:
12
+ *
13
+ * 1. `Accept` MUST name the exact type. Measured against the sandbox on
14
+ * 2026-08-26: `image/png` -> 200, while `image/*`, `*\/*`, and the header a
15
+ * browser sends for `<img>` all -> 406. Not even `image/*` is enough.
16
+ *
17
+ * 2. The type follows the STYLE, not the file name, and the default is
18
+ * Satellite:
19
+ *
20
+ * Standard -> image/png
21
+ * Satellite -> image/jpeg <- the default when `style` is omitted
22
+ *
23
+ * So the naive `Accept: image/png` is wrong for the DEFAULT request. The API
24
+ * hit the mirror image of this bug itself: it hard-coded image/png on the
25
+ * response and labelled every default render — a JPEG — as a PNG.
26
+ *
27
+ * `<img src="...">` cannot be made to work: it sends neither `Authorization` nor
28
+ * an acceptable `Accept`. Fetching to a Blob is the only viable shape, which is
29
+ * why this returns one rather than a URL.
30
+ */
31
+ /** `map`, or `map@2x` for a retina render. There is no file extension. */
32
+ export type StaticMapFileName = 'map' | 'map@2x';
33
+ export interface StaticMapOptions {
34
+ /** Pixels, 64-1500. */
35
+ width: number;
36
+ /** Pixels, 64-1500. */
37
+ height: number;
38
+ /** `[longitude, latitude]`. */
39
+ center?: [number, number];
40
+ /** `[west, south, east, north]`. */
41
+ boundingBox?: [number, number, number, number];
42
+ /** Positions the render must contain, as `[lng, lat]` pairs. */
43
+ boundedPositions?: Array<[number, number]>;
44
+ zoom?: number;
45
+ radius?: number;
46
+ padding?: number;
47
+ cropLabels?: boolean;
48
+ style?: StaticMapStyle;
49
+ colorScheme?: ColorScheme;
50
+ labelSize?: LabelSize;
51
+ /**
52
+ * `Enabled` | `Disabled` -- NOT a list of categories. api#35 records this
53
+ * being passed as a string array against the SDK's Enabled|Disabled type,
54
+ * working only by stringification. Typed here so that cannot recur.
55
+ */
56
+ pointsOfInterests?: MapFeatureMode;
57
+ scaleBarUnit?: ScaleBarUnit;
58
+ /** Defaults to `map`. */
59
+ fileName?: StaticMapFileName;
60
+ }
61
+ /**
62
+ * The Accept header this request must send.
63
+ *
64
+ * Exported because it is the one rule a caller cannot guess, and anyone doing
65
+ * their own fetch — a server-side render, a proxy — needs it too. Satellite is
66
+ * the default, so an absent style means JPEG.
67
+ */
68
+ export declare function staticMapAccept(style?: StaticMapStyle): string;
69
+ /** Build the request URL without fetching it. */
70
+ export declare function buildStaticMapUrl(apiUrl: string, options: StaticMapOptions): string;
71
+ /**
72
+ * Fetch a static map as a Blob.
73
+ *
74
+ * @param apiUrl Base URL of the Location Service API
75
+ * @param options Render options; exactly one of center / boundingBox / boundedPositions
76
+ * @param getToken Callback returning the current auth token
77
+ *
78
+ * @example
79
+ * const blob = await fetchStaticMap(API_URL, {
80
+ * width: 640, height: 400, center: [151.2093, -33.8688], zoom: 14,
81
+ * style: 'Standard',
82
+ * }, getToken)
83
+ * const url = URL.createObjectURL(blob) // remember to revokeObjectURL
84
+ */
85
+ export declare function fetchStaticMap(apiUrl: string, options: StaticMapOptions, getToken: () => string | undefined): Promise<Blob>;
@@ -0,0 +1,68 @@
1
+ import { parseErrorResponse } from '../transport/errors';
2
+ /**
3
+ * The Accept header this request must send.
4
+ *
5
+ * Exported because it is the one rule a caller cannot guess, and anyone doing
6
+ * their own fetch — a server-side render, a proxy — needs it too. Satellite is
7
+ * the default, so an absent style means JPEG.
8
+ */
9
+ export function staticMapAccept(style) {
10
+ return style === 'Standard' ? 'image/png' : 'image/jpeg';
11
+ }
12
+ /** Build the request URL without fetching it. */
13
+ export function buildStaticMapUrl(apiUrl, options) {
14
+ const { width, height, center, boundingBox, boundedPositions, fileName = 'map', ...rest } = options;
15
+ const params = new URLSearchParams({
16
+ width: String(width),
17
+ height: String(height),
18
+ });
19
+ // Positions go over the wire as comma-separated numbers. api#35 records that
20
+ // passing arrays straight through worked only by accidental stringification;
21
+ // doing it explicitly here means the shape is ours, not JavaScript's default.
22
+ if (center)
23
+ params.set('center', center.join(','));
24
+ if (boundingBox)
25
+ params.set('bounding-box', boundingBox.join(','));
26
+ if (boundedPositions) {
27
+ params.set('bounded-positions', boundedPositions.flat().join(','));
28
+ }
29
+ // The API accepts kebab-case on the wire and camelCase for older callers;
30
+ // kebab is the documented form, so emit that.
31
+ const KEBAB = { cropLabels: 'crop-labels' };
32
+ for (const [key, value] of Object.entries(rest)) {
33
+ if (value === undefined)
34
+ continue;
35
+ params.set(KEBAB[key] ?? key, String(value));
36
+ }
37
+ return `${apiUrl}/maps/static/${fileName}?${params}`;
38
+ }
39
+ /**
40
+ * Fetch a static map as a Blob.
41
+ *
42
+ * @param apiUrl Base URL of the Location Service API
43
+ * @param options Render options; exactly one of center / boundingBox / boundedPositions
44
+ * @param getToken Callback returning the current auth token
45
+ *
46
+ * @example
47
+ * const blob = await fetchStaticMap(API_URL, {
48
+ * width: 640, height: 400, center: [151.2093, -33.8688], zoom: 14,
49
+ * style: 'Standard',
50
+ * }, getToken)
51
+ * const url = URL.createObjectURL(blob) // remember to revokeObjectURL
52
+ */
53
+ export async function fetchStaticMap(apiUrl, options, getToken) {
54
+ const response = await fetch(buildStaticMapUrl(apiUrl, options), {
55
+ headers: {
56
+ Authorization: `Bearer ${getToken()}`,
57
+ Accept: staticMapAccept(options.style),
58
+ },
59
+ });
60
+ if (!response.ok) {
61
+ // Same treatment as fetchMapStyle: the API's {message, code, requestId} is
62
+ // the useful part, and a bare "failed: 400" throws it away. The messages
63
+ // here are specific and actionable -- "'width' and 'height' are required",
64
+ // "Only one of center, bounding-box or bounded-positions may be set".
65
+ throw parseErrorResponse(response.status, response.statusText, await response.text());
66
+ }
67
+ return response.blob();
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaosity/location-client",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Client library for Chaosity Location Service with AWS Location Service compatibility",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",