@chaosity/location-client 0.5.0 → 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.
@@ -7,7 +7,7 @@
7
7
  * @returns MapLibre transformRequest function
8
8
  */
9
9
  export function createTransformRequest(apiUrl, getToken) {
10
- return (url, resourceType) => {
10
+ return (url, _resourceType) => {
11
11
  if (url.startsWith(apiUrl)) {
12
12
  const token = getToken();
13
13
  if (!token) {
@@ -1,14 +1,38 @@
1
1
  import type { MapLike } from '../types';
2
2
  /**
3
- * Apply a preferred display language to all symbol layers on a MapLibre map.
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')
@@ -1,13 +1,48 @@
1
1
  /**
2
- * Apply a preferred display language to all symbol layers on a MapLibre map.
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 === 'en'
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 === 'symbol') {
30
- const textField = map.getLayoutProperty(layer.id, 'text-field');
31
- if (textField !== undefined && textField !== null) {
32
- map.setLayoutProperty(layer.id, 'text-field', expression);
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 {
@@ -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 all symbol layers within a style descriptor object.
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 === 'en'
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 === 'symbol') {
100
- const layout = layer.layout;
101
- if (layout?.['text-field'] !== undefined) {
102
- layout['text-field'] = expression;
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
  }
@@ -9,6 +9,8 @@ import { parseErrorResponse } from '../transport/errors';
9
9
  export function staticMapAccept(style) {
10
10
  return style === 'Standard' ? 'image/png' : 'image/jpeg';
11
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)));
12
14
  /** Build the request URL without fetching it. */
13
15
  export function buildStaticMapUrl(apiUrl, options) {
14
16
  const { width, height, center, boundingBox, boundedPositions, fileName = 'map', ...rest } = options;
@@ -19,12 +21,18 @@ export function buildStaticMapUrl(apiUrl, options) {
19
21
  // Positions go over the wire as comma-separated numbers. api#35 records that
20
22
  // passing arrays straight through worked only by accidental stringification;
21
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.
22
30
  if (center)
23
- params.set('center', center.join(','));
31
+ params.set('center', center.map(coord).join(','));
24
32
  if (boundingBox)
25
- params.set('bounding-box', boundingBox.join(','));
33
+ params.set('bounding-box', boundingBox.map(coord).join(','));
26
34
  if (boundedPositions) {
27
- params.set('bounded-positions', boundedPositions.flat().join(','));
35
+ params.set('bounded-positions', boundedPositions.flat().map(coord).join(','));
28
36
  }
29
37
  // The API accepts kebab-case on the wire and camelCase for older callers;
30
38
  // kebab is the documented form, so emit that.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaosity/location-client",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Client library for Chaosity Location Service with AWS Location Service compatibility",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",