@chaosity/location-client 0.4.2 → 0.5.1
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/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/maps/createTransformRequest.js +1 -1
- package/dist/maps/mapLanguage.d.ts +30 -6
- package/dist/maps/mapLanguage.js +50 -20
- package/dist/maps/mapStyle.js +16 -15
- package/dist/maps/staticMap.d.ts +85 -0
- package/dist/maps/staticMap.js +76 -0
- package/package.json +1 -1
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';
|
|
@@ -1,14 +1,38 @@
|
|
|
1
1
|
import type { MapLike } from '../types';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* AWS GeoMaps vector tiles include language-specific name properties in `name:${lang}` format
|
|
6
|
-
* (e.g. `name:en`, `name:fr`, `name:ja`). This function updates the `text-field` expression
|
|
7
|
-
* on every symbol layer to prefer the requested language, falling back to English then the
|
|
8
|
-
* default name if the preferred language is unavailable for a feature.
|
|
3
|
+
* The `text-field` expression that prefers `language`, then English, then the
|
|
4
|
+
* feature's default name.
|
|
9
5
|
*
|
|
10
6
|
* Based on the approach documented at:
|
|
11
7
|
* https://docs.aws.amazon.com/location/latest/developerguide/how-to-set-preferred-language-map.html
|
|
8
|
+
*/
|
|
9
|
+
export declare function languageExpression(language: string): unknown[];
|
|
10
|
+
/**
|
|
11
|
+
* Whether a `text-field` reads a name property — the only kind of label a
|
|
12
|
+
* language rewrite makes sense for (#28).
|
|
13
|
+
*
|
|
14
|
+
* The AWS Standard style has 62 symbol layers with a text-field and 30 of
|
|
15
|
+
* them do NOT label by name: `building_label_number` reads
|
|
16
|
+
* `addr_housenumber`, and the 29 `shield_*` layers read `shield_text` or
|
|
17
|
+
* `ref`. Replacing those with a `name:<lang>` coalesce points them at a
|
|
18
|
+
* property their features do not carry, and the house numbers and road
|
|
19
|
+
* shields disappear — for `en` too. Measured against the live sandbox on
|
|
20
|
+
* 2026-08-29; the testbed showed it as "one map has house numbers, the
|
|
21
|
+
* others don't".
|
|
22
|
+
*
|
|
23
|
+
* Serialising the expression is the simplest exact test: `name`, `name:en`,
|
|
24
|
+
* `name_en` and a literal `{name}` template all contain it, and none of the
|
|
25
|
+
* non-name properties do.
|
|
26
|
+
*/
|
|
27
|
+
export declare function labelsByName(textField: unknown): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Apply a preferred display language to the name labels on a MapLibre map.
|
|
30
|
+
*
|
|
31
|
+
* AWS GeoMaps vector tiles carry `name:${lang}` properties (`name:en`,
|
|
32
|
+
* `name:fr`, `name:ja`, …). Every symbol layer whose `text-field` reads a
|
|
33
|
+
* name is rewritten to prefer the requested language, falling back to
|
|
34
|
+
* English then the default name. Layers that label by something else — house
|
|
35
|
+
* numbers, road shields — are left exactly as the style declared them.
|
|
12
36
|
*
|
|
13
37
|
* @param map - MapLibre Map instance (or any object matching the MapLike interface)
|
|
14
38
|
* @param language - ISO 639-1 language code (e.g. 'en', 'fr', 'de', 'ja', 'zh', 'ar')
|
package/dist/maps/mapLanguage.js
CHANGED
|
@@ -1,13 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* AWS GeoMaps vector tiles include language-specific name properties in `name:${lang}` format
|
|
5
|
-
* (e.g. `name:en`, `name:fr`, `name:ja`). This function updates the `text-field` expression
|
|
6
|
-
* on every symbol layer to prefer the requested language, falling back to English then the
|
|
7
|
-
* default name if the preferred language is unavailable for a feature.
|
|
2
|
+
* The `text-field` expression that prefers `language`, then English, then the
|
|
3
|
+
* feature's default name.
|
|
8
4
|
*
|
|
9
5
|
* Based on the approach documented at:
|
|
10
6
|
* https://docs.aws.amazon.com/location/latest/developerguide/how-to-set-preferred-language-map.html
|
|
7
|
+
*/
|
|
8
|
+
export function languageExpression(language) {
|
|
9
|
+
return language === 'en'
|
|
10
|
+
? ['coalesce', ['get', 'name:en'], ['get', 'name']]
|
|
11
|
+
: [
|
|
12
|
+
'coalesce',
|
|
13
|
+
['get', `name:${language}`],
|
|
14
|
+
['get', 'name:en'],
|
|
15
|
+
['get', 'name'],
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether a `text-field` reads a name property — the only kind of label a
|
|
20
|
+
* language rewrite makes sense for (#28).
|
|
21
|
+
*
|
|
22
|
+
* The AWS Standard style has 62 symbol layers with a text-field and 30 of
|
|
23
|
+
* them do NOT label by name: `building_label_number` reads
|
|
24
|
+
* `addr_housenumber`, and the 29 `shield_*` layers read `shield_text` or
|
|
25
|
+
* `ref`. Replacing those with a `name:<lang>` coalesce points them at a
|
|
26
|
+
* property their features do not carry, and the house numbers and road
|
|
27
|
+
* shields disappear — for `en` too. Measured against the live sandbox on
|
|
28
|
+
* 2026-08-29; the testbed showed it as "one map has house numbers, the
|
|
29
|
+
* others don't".
|
|
30
|
+
*
|
|
31
|
+
* Serialising the expression is the simplest exact test: `name`, `name:en`,
|
|
32
|
+
* `name_en` and a literal `{name}` template all contain it, and none of the
|
|
33
|
+
* non-name properties do.
|
|
34
|
+
*/
|
|
35
|
+
export function labelsByName(textField) {
|
|
36
|
+
return JSON.stringify(textField)?.includes('name') ?? false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Apply a preferred display language to the name labels on a MapLibre map.
|
|
40
|
+
*
|
|
41
|
+
* AWS GeoMaps vector tiles carry `name:${lang}` properties (`name:en`,
|
|
42
|
+
* `name:fr`, `name:ja`, …). Every symbol layer whose `text-field` reads a
|
|
43
|
+
* name is rewritten to prefer the requested language, falling back to
|
|
44
|
+
* English then the default name. Layers that label by something else — house
|
|
45
|
+
* numbers, road shields — are left exactly as the style declared them.
|
|
11
46
|
*
|
|
12
47
|
* @param map - MapLibre Map instance (or any object matching the MapLike interface)
|
|
13
48
|
* @param language - ISO 639-1 language code (e.g. 'en', 'fr', 'de', 'ja', 'zh', 'ar')
|
|
@@ -17,21 +52,16 @@
|
|
|
17
52
|
*/
|
|
18
53
|
export function applyMapLanguage(map, language) {
|
|
19
54
|
try {
|
|
20
|
-
const expression = language
|
|
21
|
-
? ['coalesce', ['get', 'name:en'], ['get', 'name']]
|
|
22
|
-
: [
|
|
23
|
-
'coalesce',
|
|
24
|
-
['get', `name:${language}`],
|
|
25
|
-
['get', 'name:en'],
|
|
26
|
-
['get', 'name'],
|
|
27
|
-
];
|
|
55
|
+
const expression = languageExpression(language);
|
|
28
56
|
map.getStyle().layers.forEach((layer) => {
|
|
29
|
-
if (layer.type
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
57
|
+
if (layer.type !== 'symbol')
|
|
58
|
+
return;
|
|
59
|
+
const textField = map.getLayoutProperty(layer.id, 'text-field');
|
|
60
|
+
if (textField === undefined || textField === null)
|
|
61
|
+
return;
|
|
62
|
+
if (!labelsByName(textField))
|
|
63
|
+
return;
|
|
64
|
+
map.setLayoutProperty(layer.id, 'text-field', expression);
|
|
35
65
|
});
|
|
36
66
|
}
|
|
37
67
|
catch {
|
package/dist/maps/mapStyle.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseErrorResponse } from '../transport/errors';
|
|
2
|
+
import { labelsByName, languageExpression } from './mapLanguage';
|
|
2
3
|
/**
|
|
3
4
|
* Build a map style descriptor URL for the Location Service API.
|
|
4
5
|
*
|
|
@@ -83,24 +84,24 @@ export async function fetchMapStyle(apiUrl, mapStyle, getToken, options = {}) {
|
|
|
83
84
|
return style;
|
|
84
85
|
}
|
|
85
86
|
/**
|
|
86
|
-
* Apply a preferred language to
|
|
87
|
+
* Apply a preferred language to the name labels within a style descriptor.
|
|
87
88
|
* Mutates the style in place — call before passing to MapLibre.
|
|
89
|
+
*
|
|
90
|
+
* Only a `text-field` that reads a name property is rewritten. House numbers
|
|
91
|
+
* (`addr_housenumber`) and road shields (`shield_text`) used to be rewritten
|
|
92
|
+
* too, and vanished from every map that asked for a language (#28); the rule
|
|
93
|
+
* lives in `labelsByName` so this and `applyMapLanguage` cannot disagree.
|
|
88
94
|
*/
|
|
89
95
|
function applyLanguageToDescriptor(style, language) {
|
|
90
|
-
const expression = language
|
|
91
|
-
? ['coalesce', ['get', 'name:en'], ['get', 'name']]
|
|
92
|
-
: [
|
|
93
|
-
'coalesce',
|
|
94
|
-
['get', `name:${language}`],
|
|
95
|
-
['get', 'name:en'],
|
|
96
|
-
['get', 'name'],
|
|
97
|
-
];
|
|
96
|
+
const expression = languageExpression(language);
|
|
98
97
|
for (const layer of style.layers) {
|
|
99
|
-
if (layer.type
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
98
|
+
if (layer.type !== 'symbol')
|
|
99
|
+
continue;
|
|
100
|
+
const layout = layer.layout;
|
|
101
|
+
if (layout?.['text-field'] === undefined)
|
|
102
|
+
continue;
|
|
103
|
+
if (!labelsByName(layout['text-field']))
|
|
104
|
+
continue;
|
|
105
|
+
layout['text-field'] = expression;
|
|
105
106
|
}
|
|
106
107
|
}
|
|
@@ -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,76 @@
|
|
|
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
|
+
/** A coordinate as the API accepts it: at most six decimals, no float noise. */
|
|
13
|
+
const coord = (n) => String(Number(n.toFixed(6)));
|
|
14
|
+
/** Build the request URL without fetching it. */
|
|
15
|
+
export function buildStaticMapUrl(apiUrl, options) {
|
|
16
|
+
const { width, height, center, boundingBox, boundedPositions, fileName = 'map', ...rest } = options;
|
|
17
|
+
const params = new URLSearchParams({
|
|
18
|
+
width: String(width),
|
|
19
|
+
height: String(height),
|
|
20
|
+
});
|
|
21
|
+
// Positions go over the wire as comma-separated numbers. api#35 records that
|
|
22
|
+
// passing arrays straight through worked only by accidental stringification;
|
|
23
|
+
// doing it explicitly here means the shape is ours, not JavaScript's default.
|
|
24
|
+
//
|
|
25
|
+
// Each number is rounded to six decimals (#29). The API caps a coordinate at
|
|
26
|
+
// fourteen decimals, and a value straight from `map.getCenter()` routinely
|
|
27
|
+
// has fifteen or sixteen — so the obvious "render what the user is looking
|
|
28
|
+
// at" was a 400 blaming the FORMAT. Six decimals is ~10 cm, more than a
|
|
29
|
+
// raster render can show.
|
|
30
|
+
if (center)
|
|
31
|
+
params.set('center', center.map(coord).join(','));
|
|
32
|
+
if (boundingBox)
|
|
33
|
+
params.set('bounding-box', boundingBox.map(coord).join(','));
|
|
34
|
+
if (boundedPositions) {
|
|
35
|
+
params.set('bounded-positions', boundedPositions.flat().map(coord).join(','));
|
|
36
|
+
}
|
|
37
|
+
// The API accepts kebab-case on the wire and camelCase for older callers;
|
|
38
|
+
// kebab is the documented form, so emit that.
|
|
39
|
+
const KEBAB = { cropLabels: 'crop-labels' };
|
|
40
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
41
|
+
if (value === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
params.set(KEBAB[key] ?? key, String(value));
|
|
44
|
+
}
|
|
45
|
+
return `${apiUrl}/maps/static/${fileName}?${params}`;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Fetch a static map as a Blob.
|
|
49
|
+
*
|
|
50
|
+
* @param apiUrl Base URL of the Location Service API
|
|
51
|
+
* @param options Render options; exactly one of center / boundingBox / boundedPositions
|
|
52
|
+
* @param getToken Callback returning the current auth token
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const blob = await fetchStaticMap(API_URL, {
|
|
56
|
+
* width: 640, height: 400, center: [151.2093, -33.8688], zoom: 14,
|
|
57
|
+
* style: 'Standard',
|
|
58
|
+
* }, getToken)
|
|
59
|
+
* const url = URL.createObjectURL(blob) // remember to revokeObjectURL
|
|
60
|
+
*/
|
|
61
|
+
export async function fetchStaticMap(apiUrl, options, getToken) {
|
|
62
|
+
const response = await fetch(buildStaticMapUrl(apiUrl, options), {
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Bearer ${getToken()}`,
|
|
65
|
+
Accept: staticMapAccept(options.style),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
// Same treatment as fetchMapStyle: the API's {message, code, requestId} is
|
|
70
|
+
// the useful part, and a bare "failed: 400" throws it away. The messages
|
|
71
|
+
// here are specific and actionable -- "'width' and 'height' are required",
|
|
72
|
+
// "Only one of center, bounding-box or bounded-positions may be set".
|
|
73
|
+
throw parseErrorResponse(response.status, response.statusText, await response.text());
|
|
74
|
+
}
|
|
75
|
+
return response.blob();
|
|
76
|
+
}
|