@chaosity/location-client 0.1.10 → 0.1.13
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 +140 -7
- package/dist/adapters/GeoPlaces.d.ts +1 -1
- package/dist/adapters/GeoPlaces.js +31 -15
- package/dist/auth/TokenProvider.js +19 -8
- package/dist/client/GeoPlacesClient.d.ts +2 -2
- package/dist/client/GeoPlacesClient.js +17 -4
- package/dist/errors/LocationServiceException.d.ts +16 -0
- package/dist/errors/LocationServiceException.js +24 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +5 -3
- package/dist/maps/Utils.d.ts +2 -2
- package/dist/maps/Utils.js +1 -1
- package/dist/maps/createTransformRequest.js +1 -1
- package/dist/maps/mapLanguage.d.ts +3 -3
- package/dist/maps/mapLanguage.js +8 -3
- package/dist/maps/mapStyle.js +10 -5
- package/dist/server/LocationServiceConnector.d.ts +2 -1
- package/dist/server/LocationServiceConnector.js +24 -12
- package/dist/server/getClientConfig.d.ts +1 -1
- package/dist/server/getClientConfig.js +10 -4
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +1 -1
- package/dist/types/index.d.ts +27 -0
- package/dist/utils/roundPosition.d.ts +15 -0
- package/dist/utils/roundPosition.js +38 -0
- package/package.json +15 -3
- package/dist/server/getConnector.d.ts +0 -37
- package/dist/server/getConnector.js +0 -34
package/README.md
CHANGED
|
@@ -28,6 +28,9 @@ npm install @chaosity/location-client
|
|
|
28
28
|
- **AWS SDK Commands**: Full access to all AWS Location Service commands
|
|
29
29
|
- **Data Type Utilities**: Built-in GeoJSON conversion utilities
|
|
30
30
|
- **MapLibre Integration**: Adapter for MapLibre GL Geocoder and `createTransformRequest` helper
|
|
31
|
+
- **Map Style Control**: Fetch and customize map style descriptors with terrain, 3D buildings, traffic, and more
|
|
32
|
+
- **Map Language**: Switch map label language client-side with zero API calls
|
|
33
|
+
- **POI Layer Control**: Toggle point-of-interest categories on/off by layer
|
|
31
34
|
- **Server Utilities**: `getClientConfig()` with auto-env detection and token caching
|
|
32
35
|
|
|
33
36
|
## Quick Start
|
|
@@ -78,28 +81,74 @@ const client = new GeoPlacesClient({
|
|
|
78
81
|
})
|
|
79
82
|
|
|
80
83
|
const response = await client.send(
|
|
81
|
-
new SuggestCommand({ QueryText: 'Vancouver', MaxResults: 5 })
|
|
84
|
+
new SuggestCommand({ QueryText: 'Vancouver', MaxResults: 5 }),
|
|
82
85
|
)
|
|
83
86
|
```
|
|
84
87
|
|
|
85
88
|
### MapLibre Map Integration
|
|
86
89
|
|
|
87
|
-
Use `createTransformRequest` to
|
|
90
|
+
Use `fetchMapStyle` to fetch a style descriptor with authentication and optional customization, and `createTransformRequest` to attach Bearer tokens to all subsequent tile/glyph/sprite requests:
|
|
88
91
|
|
|
89
92
|
```typescript
|
|
90
|
-
import {
|
|
93
|
+
import {
|
|
94
|
+
fetchMapStyle,
|
|
95
|
+
createTransformRequest,
|
|
96
|
+
} from '@chaosity/location-client'
|
|
91
97
|
import maplibregl from 'maplibre-gl'
|
|
92
98
|
|
|
99
|
+
const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
|
|
100
|
+
colorScheme: 'Dark',
|
|
101
|
+
terrain: 'Terrain3D',
|
|
102
|
+
buildings: 'Buildings3D',
|
|
103
|
+
language: 'fr',
|
|
104
|
+
})
|
|
105
|
+
|
|
93
106
|
const map = new maplibregl.Map({
|
|
94
107
|
container: 'map',
|
|
95
|
-
style
|
|
108
|
+
style,
|
|
96
109
|
center: [-123.12, 49.28],
|
|
97
110
|
zoom: 10,
|
|
111
|
+
maxPitch: 85,
|
|
98
112
|
transformRequest: createTransformRequest(apiUrl, getToken),
|
|
99
113
|
})
|
|
100
114
|
```
|
|
101
115
|
|
|
102
|
-
|
|
116
|
+
### Switching Map Language
|
|
117
|
+
|
|
118
|
+
Change map label language instantly on the client side — no API calls needed:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { applyMapLanguage } from '@chaosity/location-client'
|
|
122
|
+
|
|
123
|
+
// Apply after the style is loaded
|
|
124
|
+
map.once('style.load', () => applyMapLanguage(map, 'fr'))
|
|
125
|
+
|
|
126
|
+
// Or reapply whenever the user changes language
|
|
127
|
+
applyMapLanguage(map, 'ja')
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Controlling POI Layers
|
|
131
|
+
|
|
132
|
+
Toggle point-of-interest categories on or off:
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import {
|
|
136
|
+
setPoiVisibility,
|
|
137
|
+
setAllPoiVisibility,
|
|
138
|
+
POI_CATEGORIES,
|
|
139
|
+
} from '@chaosity/location-client'
|
|
140
|
+
|
|
141
|
+
// Hide transit POIs
|
|
142
|
+
setPoiVisibility(map, 'transit', false)
|
|
143
|
+
|
|
144
|
+
// Hide multiple categories at once
|
|
145
|
+
setPoiVisibility(map, ['shopping', 'business'], false)
|
|
146
|
+
|
|
147
|
+
// Hide all POIs
|
|
148
|
+
setAllPoiVisibility(map, false)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Available categories: `food_drink`, `entertainment`, `sights`, `transit`, `accommodations`, `leisure`, `shopping`, `business`, `facilities`, `areas`, `parks`.
|
|
103
152
|
|
|
104
153
|
### MapLibre Geocoder Integration
|
|
105
154
|
|
|
@@ -172,6 +221,80 @@ import { createTransformRequest } from '@chaosity/location-client'
|
|
|
172
221
|
const transformRequest = createTransformRequest(apiUrl, () => currentToken)
|
|
173
222
|
```
|
|
174
223
|
|
|
224
|
+
#### fetchMapStyle
|
|
225
|
+
|
|
226
|
+
Fetches the map style descriptor with Bearer auth and applies optional language to the descriptor JSON before MapLibre processes it (eliminates the visual flash that occurs when modifying layers post-load).
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { fetchMapStyle } from '@chaosity/location-client'
|
|
230
|
+
|
|
231
|
+
const style = await fetchMapStyle(apiUrl, 'Standard', getToken, {
|
|
232
|
+
colorScheme: 'Dark',
|
|
233
|
+
terrain: 'Terrain3D',
|
|
234
|
+
buildings: 'Buildings3D',
|
|
235
|
+
contourDensity: 'Medium',
|
|
236
|
+
traffic: 'All',
|
|
237
|
+
travelModes: ['Truck', 'Transit'],
|
|
238
|
+
language: 'fr',
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const map = new maplibregl.Map({
|
|
242
|
+
style,
|
|
243
|
+
transformRequest: createTransformRequest(apiUrl, getToken),
|
|
244
|
+
})
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
#### buildMapStyleUrl
|
|
248
|
+
|
|
249
|
+
Builds the style descriptor URL without fetching. Useful when you want to pass the URL directly to MapLibre (e.g. without pre-applying language).
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
import { buildMapStyleUrl } from '@chaosity/location-client'
|
|
253
|
+
|
|
254
|
+
const url = buildMapStyleUrl(apiUrl, 'Standard', {
|
|
255
|
+
colorScheme: 'Dark',
|
|
256
|
+
terrain: 'Hillshade',
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### MapStyleOptions
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
interface MapStyleOptions {
|
|
264
|
+
colorScheme?: 'Light' | 'Dark'
|
|
265
|
+
politicalView?: string // ISO 3166-1 alpha-3 (e.g. 'IND', 'TUR')
|
|
266
|
+
terrain?: 'Hillshade' | 'Terrain3D'
|
|
267
|
+
buildings?: 'Buildings3D'
|
|
268
|
+
contourDensity?: 'Medium' // Only 'Medium' is supported by the AWS SDK
|
|
269
|
+
traffic?: 'All'
|
|
270
|
+
travelModes?: Array<'Truck' | 'Transit'>
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### applyMapLanguage
|
|
275
|
+
|
|
276
|
+
Modifies symbol layer `text-field` expressions on an existing map to display labels in the specified language. No API call — operates entirely on the client.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
import { applyMapLanguage } from '@chaosity/location-client'
|
|
280
|
+
|
|
281
|
+
applyMapLanguage(map, 'fr')
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### POI Layer Control
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import {
|
|
288
|
+
setPoiVisibility,
|
|
289
|
+
setAllPoiVisibility,
|
|
290
|
+
POI_CATEGORIES,
|
|
291
|
+
} from '@chaosity/location-client'
|
|
292
|
+
import type { PoiCategory } from '@chaosity/location-client'
|
|
293
|
+
|
|
294
|
+
setPoiVisibility(map, 'transit', false)
|
|
295
|
+
setAllPoiVisibility(map, false)
|
|
296
|
+
```
|
|
297
|
+
|
|
175
298
|
#### Available Commands
|
|
176
299
|
|
|
177
300
|
All AWS Location Service commands from `@aws-sdk/client-geo-places`:
|
|
@@ -240,9 +363,19 @@ const connector = new LocationServiceConnector({
|
|
|
240
363
|
token: config.token,
|
|
241
364
|
})
|
|
242
365
|
|
|
243
|
-
const result = await connector.send(
|
|
366
|
+
const result = await connector.send(
|
|
367
|
+
new SuggestCommand({ QueryText: 'Vancouver' }),
|
|
368
|
+
)
|
|
244
369
|
```
|
|
245
370
|
|
|
371
|
+
## Cache-Friendly Position Rounding
|
|
372
|
+
|
|
373
|
+
`BiasPosition` coordinates are automatically rounded to 2 decimal places (~1.1 km grid) before each API request. This maximizes cache hits across nearby users without affecting result quality — bias is approximate by nature.
|
|
374
|
+
|
|
375
|
+
`QueryPosition` (reverse geocode) retains full precision since it represents an exact point the user selected.
|
|
376
|
+
|
|
377
|
+
This is handled transparently in both `GeoPlacesClient` and `LocationServiceConnector` — no action needed in application code.
|
|
378
|
+
|
|
246
379
|
## Logging
|
|
247
380
|
|
|
248
381
|
The library uses the `debug` package for optional verbose logging:
|
|
@@ -266,7 +399,7 @@ Full TypeScript support with types from AWS SDK:
|
|
|
266
399
|
import type { SuggestCommandOutput } from '@aws-sdk/client-geo-places'
|
|
267
400
|
|
|
268
401
|
const response: SuggestCommandOutput = await client.send(
|
|
269
|
-
new SuggestCommand({ QueryText: 'Vancouver' })
|
|
402
|
+
new SuggestCommand({ QueryText: 'Vancouver' }),
|
|
270
403
|
)
|
|
271
404
|
```
|
|
272
405
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MaplibreGeocoderApi, MaplibreGeocoderApiConfig, MaplibreGeocoderFeatureResults, MaplibreGeocoderPlaceResults, MaplibreGeocoderSuggestionResults } from '@maplibre/maplibre-gl-geocoder';
|
|
2
2
|
import type { Map } from 'maplibre-gl';
|
|
3
|
-
import { GeoPlacesClient } from '../client/GeoPlacesClient';
|
|
3
|
+
import type { GeoPlacesClient } from '../client/GeoPlacesClient';
|
|
4
4
|
/**
|
|
5
5
|
* GeoPlaces - MapLibre adapter for AWS Location Service
|
|
6
6
|
*
|
|
@@ -33,11 +33,15 @@ export class GeoPlaces {
|
|
|
33
33
|
};
|
|
34
34
|
if (config.countries) {
|
|
35
35
|
commandInput.Filter = {
|
|
36
|
-
IncludeCountries: Array.isArray(config.countries)
|
|
36
|
+
IncludeCountries: Array.isArray(config.countries)
|
|
37
|
+
? config.countries
|
|
38
|
+
: config.countries.split(','),
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
|
-
const response = await this.client.send(new GeocodeCommand(commandInput));
|
|
40
|
-
const result = geocodeResponseToFeatureCollection(response, {
|
|
41
|
+
const response = (await this.client.send(new GeocodeCommand(commandInput)));
|
|
42
|
+
const result = geocodeResponseToFeatureCollection(response, {
|
|
43
|
+
flattenProperties: true,
|
|
44
|
+
});
|
|
41
45
|
log('forwardGeocode returned %d results', result.features?.length ?? 0);
|
|
42
46
|
return result;
|
|
43
47
|
}
|
|
@@ -51,8 +55,10 @@ export class GeoPlaces {
|
|
|
51
55
|
MaxResults: config.limit || 1,
|
|
52
56
|
Language: this.normalizeLanguage(config.language),
|
|
53
57
|
};
|
|
54
|
-
const response = await this.client.send(new ReverseGeocodeCommand(commandInput));
|
|
55
|
-
const result = reverseGeocodeResponseToFeatureCollection(response, {
|
|
58
|
+
const response = (await this.client.send(new ReverseGeocodeCommand(commandInput)));
|
|
59
|
+
const result = reverseGeocodeResponseToFeatureCollection(response, {
|
|
60
|
+
flattenProperties: true,
|
|
61
|
+
});
|
|
56
62
|
log('reverseGeocode returned %d results', result.features?.length ?? 0);
|
|
57
63
|
return result;
|
|
58
64
|
}
|
|
@@ -68,14 +74,22 @@ export class GeoPlaces {
|
|
|
68
74
|
MaxResults: config.limit || 5,
|
|
69
75
|
Language: this.normalizeLanguage(config.language),
|
|
70
76
|
AdditionalFeatures: [SuggestAdditionalFeature.CORE],
|
|
71
|
-
...(config.countries || config.bbox
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
...(config.countries || config.bbox
|
|
78
|
+
? {
|
|
79
|
+
Filter: {
|
|
80
|
+
...(config.countries
|
|
81
|
+
? {
|
|
82
|
+
IncludeCountries: Array.isArray(config.countries)
|
|
83
|
+
? config.countries
|
|
84
|
+
: config.countries.split(','),
|
|
85
|
+
}
|
|
86
|
+
: {}),
|
|
87
|
+
...(config.bbox ? { BoundingBox: config.bbox } : {}),
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
: {}),
|
|
77
91
|
};
|
|
78
|
-
const response = await this.client.send(new SuggestCommand(commandInput));
|
|
92
|
+
const response = (await this.client.send(new SuggestCommand(commandInput)));
|
|
79
93
|
const suggestions = { suggestions: [] };
|
|
80
94
|
for (const item of response.ResultItems ?? []) {
|
|
81
95
|
const text = item.Title;
|
|
@@ -99,9 +113,11 @@ export class GeoPlaces {
|
|
|
99
113
|
GetPlaceAdditionalFeature.TIME_ZONE,
|
|
100
114
|
],
|
|
101
115
|
});
|
|
102
|
-
const response = await this.client.send(command);
|
|
103
|
-
const result = getPlaceResponseToFeatureCollection(response, {
|
|
104
|
-
|
|
116
|
+
const response = (await this.client.send(command));
|
|
117
|
+
const result = getPlaceResponseToFeatureCollection(response, {
|
|
118
|
+
flattenProperties: true,
|
|
119
|
+
});
|
|
120
|
+
const carmenGeojsonFeatures = result.features.map((feature) => ({
|
|
105
121
|
...feature,
|
|
106
122
|
text: response.Title || '',
|
|
107
123
|
place_name: response.Address?.Label || '',
|
|
@@ -41,12 +41,14 @@ export class TokenProvider {
|
|
|
41
41
|
}
|
|
42
42
|
async getToken(forceRefresh = false) {
|
|
43
43
|
if (!forceRefresh && this.cachedToken && !this.isExpired()) {
|
|
44
|
-
const expiresIn = this.cachedExpiresAt
|
|
44
|
+
const expiresIn = this.cachedExpiresAt
|
|
45
|
+
? Math.floor((this.cachedExpiresAt - Date.now()) / 1000)
|
|
46
|
+
: 'unknown';
|
|
45
47
|
log('Using cached token (expires in %ds)', expiresIn);
|
|
46
48
|
return {
|
|
47
49
|
success: true,
|
|
48
50
|
token: this.cachedToken,
|
|
49
|
-
expiresAt: this.cachedExpiresAt
|
|
51
|
+
expiresAt: this.cachedExpiresAt,
|
|
50
52
|
};
|
|
51
53
|
}
|
|
52
54
|
// If token fetch is already in progress, wait for it
|
|
@@ -55,7 +57,11 @@ export class TokenProvider {
|
|
|
55
57
|
return this.tokenPromise;
|
|
56
58
|
}
|
|
57
59
|
// Start new token fetch
|
|
58
|
-
const reason = forceRefresh
|
|
60
|
+
const reason = forceRefresh
|
|
61
|
+
? 'forced refresh'
|
|
62
|
+
: this.cachedToken
|
|
63
|
+
? 'token expired'
|
|
64
|
+
: 'no cached token';
|
|
59
65
|
log('Refreshing token (%s) from %s', reason, this.config.apiUrl);
|
|
60
66
|
this.tokenPromise = this.fetchToken();
|
|
61
67
|
try {
|
|
@@ -74,10 +80,12 @@ export class TokenProvider {
|
|
|
74
80
|
const response = await fetch(`${apiUrl}/auth/token`, {
|
|
75
81
|
method: 'POST',
|
|
76
82
|
headers: {
|
|
77
|
-
|
|
83
|
+
Authorization: `Basic ${credentials}`,
|
|
78
84
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
79
85
|
},
|
|
80
|
-
body: new URLSearchParams({
|
|
86
|
+
body: new URLSearchParams({
|
|
87
|
+
grant_type: 'client_credentials',
|
|
88
|
+
}).toString(),
|
|
81
89
|
});
|
|
82
90
|
if (!response.ok) {
|
|
83
91
|
const errorText = await response.text();
|
|
@@ -89,13 +97,16 @@ export class TokenProvider {
|
|
|
89
97
|
else if (errorData.error)
|
|
90
98
|
errorMessage = errorData.error;
|
|
91
99
|
}
|
|
92
|
-
catch {
|
|
100
|
+
catch {
|
|
101
|
+
/* use statusText */
|
|
102
|
+
}
|
|
93
103
|
throw new Error(errorMessage);
|
|
94
104
|
}
|
|
95
105
|
const data = await response.json();
|
|
96
106
|
this.cachedToken = data.access_token;
|
|
97
107
|
// Prefer absolute expires_at (ms) from response, fall back to expires_in (seconds)
|
|
98
|
-
this.cachedExpiresAt =
|
|
108
|
+
this.cachedExpiresAt =
|
|
109
|
+
data.expires_at ?? Date.now() + (data.expires_in ?? 900) * 1000;
|
|
99
110
|
const expiresInSec = Math.floor((this.cachedExpiresAt - Date.now()) / 1000);
|
|
100
111
|
log('Token acquired successfully (expires in %ds)', expiresInSec);
|
|
101
112
|
return {
|
|
@@ -115,7 +126,7 @@ export class TokenProvider {
|
|
|
115
126
|
isExpired(bufferSeconds = 60) {
|
|
116
127
|
if (!this.cachedExpiresAt)
|
|
117
128
|
return true;
|
|
118
|
-
return Date.now() >=
|
|
129
|
+
return Date.now() >= this.cachedExpiresAt - bufferSeconds * 1000;
|
|
119
130
|
}
|
|
120
131
|
clearCache() {
|
|
121
132
|
this.cachedToken = undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ClientConfig } from '../types';
|
|
1
|
+
import type { ClientConfig, GeoPlacesCommand } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* GeoPlacesClient - AWS Location Service compatible client with custom auth
|
|
4
4
|
*
|
|
@@ -13,6 +13,6 @@ export declare class GeoPlacesClient {
|
|
|
13
13
|
serviceId: string;
|
|
14
14
|
};
|
|
15
15
|
constructor(config: ClientConfig);
|
|
16
|
-
send<
|
|
16
|
+
send<TOutput>(command: GeoPlacesCommand): Promise<TOutput>;
|
|
17
17
|
private getEndpoint;
|
|
18
18
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import debug from 'debug';
|
|
2
1
|
import { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, } from '@aws-sdk/client-geo-places';
|
|
2
|
+
import debug from 'debug';
|
|
3
|
+
import { LocationServiceException } from '../errors/LocationServiceException';
|
|
4
|
+
import { roundPositionFields } from '../utils/roundPosition';
|
|
3
5
|
const log = debug('location-client:api');
|
|
4
6
|
const logError = debug('location-client:api:error');
|
|
5
7
|
// Minification-safe endpoint map — uses constructor identity, not class name strings
|
|
@@ -29,7 +31,7 @@ export class GeoPlacesClient {
|
|
|
29
31
|
const endpoint = this.getEndpoint(command);
|
|
30
32
|
const url = `${this.clientConfig.apiUrl}${endpoint}`;
|
|
31
33
|
const commandName = command.constructor.name;
|
|
32
|
-
const input = command.input;
|
|
34
|
+
const input = roundPositionFields(command.input);
|
|
33
35
|
// Prefer getToken callback (live ref) over static token string
|
|
34
36
|
const token = this.clientConfig.getToken?.() ?? this.clientConfig.token;
|
|
35
37
|
log('Sending %s to %s', commandName, endpoint);
|
|
@@ -38,7 +40,7 @@ export class GeoPlacesClient {
|
|
|
38
40
|
method: 'POST',
|
|
39
41
|
headers: {
|
|
40
42
|
'Content-Type': 'application/json',
|
|
41
|
-
|
|
43
|
+
Authorization: `Bearer ${token}`,
|
|
42
44
|
},
|
|
43
45
|
body: JSON.stringify(input),
|
|
44
46
|
});
|
|
@@ -47,16 +49,27 @@ export class GeoPlacesClient {
|
|
|
47
49
|
const errorText = await response.text();
|
|
48
50
|
logError('Request failed: %s %s (%dms)', response.status, response.statusText, duration);
|
|
49
51
|
let errorMessage = `API request failed: ${response.statusText}`;
|
|
52
|
+
let errorCode = 'ServiceException';
|
|
53
|
+
let requestId;
|
|
50
54
|
try {
|
|
51
55
|
const errorData = JSON.parse(errorText);
|
|
52
56
|
if (errorData.message)
|
|
53
57
|
errorMessage = errorData.message;
|
|
58
|
+
if (errorData.code)
|
|
59
|
+
errorCode = errorData.code;
|
|
60
|
+
if (errorData.requestId)
|
|
61
|
+
requestId = errorData.requestId;
|
|
54
62
|
}
|
|
55
63
|
catch {
|
|
56
64
|
if (errorText)
|
|
57
65
|
errorMessage = errorText;
|
|
58
66
|
}
|
|
59
|
-
throw new
|
|
67
|
+
throw new LocationServiceException({
|
|
68
|
+
message: errorMessage,
|
|
69
|
+
code: errorCode,
|
|
70
|
+
statusCode: response.status,
|
|
71
|
+
requestId,
|
|
72
|
+
});
|
|
60
73
|
}
|
|
61
74
|
const result = await response.json();
|
|
62
75
|
log('Request successful: %s (%dms)', response.status, duration);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface LocationServiceExceptionOptions {
|
|
2
|
+
message: string;
|
|
3
|
+
code: string;
|
|
4
|
+
statusCode: number;
|
|
5
|
+
requestId?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare class LocationServiceException extends Error {
|
|
8
|
+
readonly code: string;
|
|
9
|
+
readonly statusCode: number;
|
|
10
|
+
readonly requestId?: string;
|
|
11
|
+
constructor(options: LocationServiceExceptionOptions);
|
|
12
|
+
get isRetryable(): boolean;
|
|
13
|
+
get isThrottling(): boolean;
|
|
14
|
+
get isValidation(): boolean;
|
|
15
|
+
toString(): string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export class LocationServiceException extends Error {
|
|
2
|
+
constructor(options) {
|
|
3
|
+
super(options.message);
|
|
4
|
+
this.name = 'LocationServiceException';
|
|
5
|
+
this.code = options.code;
|
|
6
|
+
this.statusCode = options.statusCode;
|
|
7
|
+
this.requestId = options.requestId;
|
|
8
|
+
}
|
|
9
|
+
get isRetryable() {
|
|
10
|
+
return this.statusCode === 429 || this.statusCode >= 500;
|
|
11
|
+
}
|
|
12
|
+
get isThrottling() {
|
|
13
|
+
return this.code === 'ThrottlingException' || this.statusCode === 429;
|
|
14
|
+
}
|
|
15
|
+
get isValidation() {
|
|
16
|
+
return this.code === 'ValidationException' || this.statusCode === 400;
|
|
17
|
+
}
|
|
18
|
+
toString() {
|
|
19
|
+
const parts = [`LocationServiceException: [${this.code}] ${this.message}`];
|
|
20
|
+
if (this.requestId)
|
|
21
|
+
parts.push(`(requestId: ${this.requestId})`);
|
|
22
|
+
return parts.join(' ');
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export { GeoPlacesClient } from './client/GeoPlacesClient';
|
|
2
|
+
export { LocationServiceException } from './errors/LocationServiceException';
|
|
3
|
+
export type { LocationServiceExceptionOptions } from './errors/LocationServiceException';
|
|
2
4
|
export * from '@aws-sdk/client-geo-places';
|
|
3
5
|
export * from '@aws/amazon-location-utilities-datatypes';
|
|
4
6
|
export { GeoPlaces } from './adapters/GeoPlaces';
|
|
5
7
|
export { createTransformRequest } from './maps/createTransformRequest';
|
|
6
|
-
export { transformRequest } from './maps/Utils';
|
|
7
|
-
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
|
|
8
|
-
export type { MapStyleOptions } from './maps/mapStyle';
|
|
9
8
|
export { applyMapLanguage } from './maps/mapLanguage';
|
|
10
|
-
export {
|
|
9
|
+
export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi';
|
|
11
10
|
export type { PoiCategory } from './maps/mapPoi';
|
|
12
|
-
export
|
|
11
|
+
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
|
|
12
|
+
export type { MapStyleOptions } from './maps/mapStyle';
|
|
13
|
+
export { transformRequest } from './maps/Utils';
|
|
14
|
+
export type { ClientConfig, GeoPlacesCommand, MapLike } from './types';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Client (Custom - uses our auth instead of AWS SigV4)
|
|
2
2
|
export { GeoPlacesClient } from './client/GeoPlacesClient';
|
|
3
|
+
// Errors
|
|
4
|
+
export { LocationServiceException } from './errors/LocationServiceException';
|
|
3
5
|
// Re-export AWS SDK commands and types
|
|
4
6
|
export * from '@aws-sdk/client-geo-places';
|
|
5
7
|
// Re-export AWS Location Utilities (data type conversions)
|
|
@@ -8,8 +10,8 @@ export * from '@aws/amazon-location-utilities-datatypes';
|
|
|
8
10
|
export { GeoPlaces } from './adapters/GeoPlaces';
|
|
9
11
|
// Maps utilities
|
|
10
12
|
export { createTransformRequest } from './maps/createTransformRequest';
|
|
11
|
-
export { transformRequest } from './maps/Utils';
|
|
12
|
-
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
|
|
13
13
|
export { applyMapLanguage } from './maps/mapLanguage';
|
|
14
|
-
export {
|
|
14
|
+
export { POI_CATEGORIES, setAllPoiVisibility, setPoiVisibility, } from './maps/mapPoi';
|
|
15
|
+
export { buildMapStyleUrl, fetchMapStyle } from './maps/mapStyle';
|
|
16
|
+
export { transformRequest } from './maps/Utils';
|
|
15
17
|
// Server-only utilities are available via '@chaosity/location-client/server'
|
package/dist/maps/Utils.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { ClientConfig } from
|
|
2
|
-
export declare function transformRequest(url: string, config: ClientConfig): import("maplibre-gl").RequestParameters | undefined;
|
|
1
|
+
import type { ClientConfig } from '../types';
|
|
2
|
+
export declare function transformRequest(url: string, config: ClientConfig): import("maplibre-gl").RequestParameters | Promise<import("maplibre-gl").RequestParameters> | undefined;
|
package/dist/maps/Utils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createTransformRequest } from
|
|
1
|
+
import { createTransformRequest } from './createTransformRequest';
|
|
2
2
|
export function transformRequest(url, config) {
|
|
3
3
|
const token = config.getToken?.() ?? config.token;
|
|
4
4
|
return createTransformRequest(config.apiUrl, () => token)(url);
|
|
@@ -15,7 +15,7 @@ export function createTransformRequest(apiUrl, getToken) {
|
|
|
15
15
|
return { url };
|
|
16
16
|
}
|
|
17
17
|
const headers = {
|
|
18
|
-
|
|
18
|
+
Authorization: `Bearer ${token}`,
|
|
19
19
|
};
|
|
20
20
|
// Set appropriate Accept headers based on resource type
|
|
21
21
|
if (url.includes('/tiles/')) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MapLike } from '../types';
|
|
2
2
|
/**
|
|
3
3
|
* Apply a preferred display language to all symbol layers on a MapLibre map.
|
|
4
4
|
*
|
|
@@ -10,10 +10,10 @@ import type { Map } from 'maplibre-gl';
|
|
|
10
10
|
* Based on the approach documented at:
|
|
11
11
|
* https://docs.aws.amazon.com/location/latest/developerguide/how-to-set-preferred-language-map.html
|
|
12
12
|
*
|
|
13
|
-
* @param map - MapLibre Map instance
|
|
13
|
+
* @param map - MapLibre Map instance (or any object matching the MapLike interface)
|
|
14
14
|
* @param language - ISO 639-1 language code (e.g. 'en', 'fr', 'de', 'ja', 'zh', 'ar')
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* map.once('style.load', () => applyMapLanguage(map, 'fr'))
|
|
18
18
|
*/
|
|
19
|
-
export declare function applyMapLanguage(map:
|
|
19
|
+
export declare function applyMapLanguage(map: MapLike, language: string): void;
|
package/dist/maps/mapLanguage.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Based on the approach documented at:
|
|
10
10
|
* https://docs.aws.amazon.com/location/latest/developerguide/how-to-set-preferred-language-map.html
|
|
11
11
|
*
|
|
12
|
-
* @param map - MapLibre Map instance
|
|
12
|
+
* @param map - MapLibre Map instance (or any object matching the MapLike interface)
|
|
13
13
|
* @param language - ISO 639-1 language code (e.g. 'en', 'fr', 'de', 'ja', 'zh', 'ar')
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
@@ -19,8 +19,13 @@ export function applyMapLanguage(map, language) {
|
|
|
19
19
|
try {
|
|
20
20
|
const expression = language === 'en'
|
|
21
21
|
? ['coalesce', ['get', 'name:en'], ['get', 'name']]
|
|
22
|
-
: [
|
|
23
|
-
|
|
22
|
+
: [
|
|
23
|
+
'coalesce',
|
|
24
|
+
['get', `name:${language}`],
|
|
25
|
+
['get', 'name:en'],
|
|
26
|
+
['get', 'name'],
|
|
27
|
+
];
|
|
28
|
+
map.getStyle().layers.forEach((layer) => {
|
|
24
29
|
if (layer.type === 'symbol') {
|
|
25
30
|
const textField = map.getLayoutProperty(layer.id, 'text-field');
|
|
26
31
|
if (textField !== undefined && textField !== null) {
|
package/dist/maps/mapStyle.js
CHANGED
|
@@ -56,14 +56,14 @@ export async function fetchMapStyle(apiUrl, mapStyle, getToken, options = {}) {
|
|
|
56
56
|
const token = getToken();
|
|
57
57
|
const response = await fetch(url, {
|
|
58
58
|
headers: {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
59
|
+
Authorization: `Bearer ${token}`,
|
|
60
|
+
Accept: 'application/json',
|
|
61
|
+
},
|
|
62
62
|
});
|
|
63
63
|
if (!response.ok) {
|
|
64
64
|
throw new Error(`Failed to fetch map style: ${response.status} ${response.statusText}`);
|
|
65
65
|
}
|
|
66
|
-
const style = await response.json();
|
|
66
|
+
const style = (await response.json());
|
|
67
67
|
if (language) {
|
|
68
68
|
applyLanguageToDescriptor(style, language);
|
|
69
69
|
}
|
|
@@ -76,7 +76,12 @@ export async function fetchMapStyle(apiUrl, mapStyle, getToken, options = {}) {
|
|
|
76
76
|
function applyLanguageToDescriptor(style, language) {
|
|
77
77
|
const expression = language === 'en'
|
|
78
78
|
? ['coalesce', ['get', 'name:en'], ['get', 'name']]
|
|
79
|
-
: [
|
|
79
|
+
: [
|
|
80
|
+
'coalesce',
|
|
81
|
+
['get', `name:${language}`],
|
|
82
|
+
['get', 'name:en'],
|
|
83
|
+
['get', 'name'],
|
|
84
|
+
];
|
|
80
85
|
for (const layer of style.layers) {
|
|
81
86
|
if (layer.type === 'symbol') {
|
|
82
87
|
const layout = layer.layout;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GeoPlacesCommand } from '../types';
|
|
1
2
|
export interface ConnectorConfig {
|
|
2
3
|
apiUrl?: string;
|
|
3
4
|
token?: string;
|
|
@@ -41,6 +42,6 @@ export declare class LocationServiceConnector {
|
|
|
41
42
|
private configPromise;
|
|
42
43
|
readonly serviceId: string;
|
|
43
44
|
constructor(config?: ConnectorConfig);
|
|
44
|
-
send<
|
|
45
|
+
send<TOutput>(command: GeoPlacesCommand, options?: SendOptions): Promise<TOutput>;
|
|
45
46
|
private getEndpoint;
|
|
46
47
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AutocompleteCommand, GeocodeCommand, GetPlaceCommand, ReverseGeocodeCommand, SearchNearbyCommand, SearchTextCommand, SuggestCommand, } from '@aws-sdk/client-geo-places';
|
|
2
2
|
import debug from 'debug';
|
|
3
|
+
import { LocationServiceException } from '../errors/LocationServiceException';
|
|
4
|
+
import { roundPositionFields } from '../utils/roundPosition';
|
|
3
5
|
import { getClientConfig } from './getClientConfig';
|
|
4
6
|
const log = debug('location-client:connector');
|
|
5
7
|
/**
|
|
@@ -34,9 +36,7 @@ const log = debug('location-client:connector');
|
|
|
34
36
|
export class LocationServiceConnector {
|
|
35
37
|
constructor(config) {
|
|
36
38
|
this.serviceId = 'Geo Places';
|
|
37
|
-
this.configPromise = config
|
|
38
|
-
? Promise.resolve(config)
|
|
39
|
-
: getClientConfig();
|
|
39
|
+
this.configPromise = config ? Promise.resolve(config) : getClientConfig();
|
|
40
40
|
}
|
|
41
41
|
async send(command, options) {
|
|
42
42
|
const config = await this.configPromise;
|
|
@@ -44,7 +44,11 @@ export class LocationServiceConnector {
|
|
|
44
44
|
let token;
|
|
45
45
|
if ('getToken' in config && typeof config.getToken === 'function') {
|
|
46
46
|
const result = await config.getToken();
|
|
47
|
-
token = result
|
|
47
|
+
token = result
|
|
48
|
+
? typeof result === 'string'
|
|
49
|
+
? result
|
|
50
|
+
: result.token
|
|
51
|
+
: undefined;
|
|
48
52
|
}
|
|
49
53
|
else {
|
|
50
54
|
token = config.token;
|
|
@@ -55,38 +59,46 @@ export class LocationServiceConnector {
|
|
|
55
59
|
const endpoint = this.getEndpoint(command);
|
|
56
60
|
const url = `${config.apiUrl}${endpoint}`;
|
|
57
61
|
const commandName = command.constructor.name;
|
|
58
|
-
const input = command.input;
|
|
62
|
+
const input = roundPositionFields(command.input);
|
|
59
63
|
log('Sending %s request to %s', commandName, endpoint);
|
|
60
64
|
const startTime = Date.now();
|
|
61
65
|
// Merge headers: user headers, then system headers override
|
|
62
66
|
const headers = {
|
|
63
67
|
...(options?.headers || {}),
|
|
64
68
|
'Content-Type': 'application/json',
|
|
65
|
-
|
|
69
|
+
Authorization: `Bearer ${token}`,
|
|
66
70
|
};
|
|
67
71
|
const response = await fetch(url, {
|
|
68
72
|
method: 'POST',
|
|
69
73
|
headers,
|
|
70
|
-
body: JSON.stringify(input)
|
|
74
|
+
body: JSON.stringify(input),
|
|
71
75
|
});
|
|
72
76
|
const duration = Date.now() - startTime;
|
|
73
77
|
if (!response.ok) {
|
|
74
78
|
const errorText = await response.text();
|
|
75
79
|
log('Request failed: %s %s (%dms)', response.status, response.statusText, duration);
|
|
76
|
-
// Try to parse error message from response
|
|
77
80
|
let errorMessage = `API request failed: ${response.statusText}`;
|
|
81
|
+
let errorCode = 'ServiceException';
|
|
82
|
+
let requestId;
|
|
78
83
|
try {
|
|
79
84
|
const errorData = JSON.parse(errorText);
|
|
80
|
-
if (errorData.message)
|
|
85
|
+
if (errorData.message)
|
|
81
86
|
errorMessage = errorData.message;
|
|
82
|
-
|
|
87
|
+
if (errorData.code)
|
|
88
|
+
errorCode = errorData.code;
|
|
89
|
+
if (errorData.requestId)
|
|
90
|
+
requestId = errorData.requestId;
|
|
83
91
|
}
|
|
84
92
|
catch {
|
|
85
|
-
// If not JSON, use raw text if available
|
|
86
93
|
if (errorText)
|
|
87
94
|
errorMessage = errorText;
|
|
88
95
|
}
|
|
89
|
-
throw new
|
|
96
|
+
throw new LocationServiceException({
|
|
97
|
+
message: errorMessage,
|
|
98
|
+
code: errorCode,
|
|
99
|
+
statusCode: response.status,
|
|
100
|
+
requestId,
|
|
101
|
+
});
|
|
90
102
|
}
|
|
91
103
|
const result = await response.json();
|
|
92
104
|
log('Request successful: %s (%dms)', response.status, duration);
|
|
@@ -48,7 +48,10 @@ function getTokenProvider(apiUrl, clientId, clientSecret) {
|
|
|
48
48
|
* const connector = new LocationServiceConnector({ apiUrl: config.apiUrl, token })
|
|
49
49
|
*/
|
|
50
50
|
export async function getClientConfig(config = {}) {
|
|
51
|
-
log('[getClientConfig] Starting with config:', {
|
|
51
|
+
log('[getClientConfig] Starting with config:', {
|
|
52
|
+
hasApiUrl: !!config.apiUrl,
|
|
53
|
+
hasClientId: !!config.clientId,
|
|
54
|
+
});
|
|
52
55
|
// Auto-detect from environment with fallbacks
|
|
53
56
|
const apiUrl = config.apiUrl ||
|
|
54
57
|
process.env.LOCATION_API_URL ||
|
|
@@ -59,7 +62,10 @@ export async function getClientConfig(config = {}) {
|
|
|
59
62
|
const clientSecret = config.clientSecret ||
|
|
60
63
|
process.env.LOCATION_CLIENT_SECRET ||
|
|
61
64
|
process.env.LOCATION_SERVICE_CLIENT_SECRET;
|
|
62
|
-
log('[getClientConfig] Resolved config:', {
|
|
65
|
+
log('[getClientConfig] Resolved config:', {
|
|
66
|
+
apiUrl,
|
|
67
|
+
clientId: clientId?.substring(0, 10) + '...',
|
|
68
|
+
});
|
|
63
69
|
// Validate required values
|
|
64
70
|
if (!apiUrl || !clientId || !clientSecret) {
|
|
65
71
|
console.error('[getClientConfig] Missing required configuration');
|
|
@@ -77,12 +83,12 @@ export async function getClientConfig(config = {}) {
|
|
|
77
83
|
throw new Error(isAuthError
|
|
78
84
|
? `Authentication failed for client ID "${clientId}". ` +
|
|
79
85
|
`Verify LOCATION_CLIENT_ID and LOCATION_CLIENT_SECRET match your application in the developer portal.`
|
|
80
|
-
:
|
|
86
|
+
: result.error || 'Failed to get token');
|
|
81
87
|
}
|
|
82
88
|
log('[getClientConfig] Token fetched successfully, length:', result.token.length);
|
|
83
89
|
return {
|
|
84
90
|
apiUrl,
|
|
85
91
|
token: result.token,
|
|
86
|
-
expiresAt: result.expiresAt
|
|
92
|
+
expiresAt: result.expiresAt,
|
|
87
93
|
};
|
|
88
94
|
}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
export { TokenProvider } from '../auth/TokenProvider';
|
|
2
|
+
export type { TokenProviderConfig, TokenResponse } from '../auth/TokenProvider';
|
|
1
3
|
export { getClientConfig } from './getClientConfig';
|
|
2
4
|
export type { ServerAuthConfig, ServerClientConfig } from './getClientConfig';
|
|
3
5
|
export { LocationServiceConnector } from './LocationServiceConnector';
|
|
4
6
|
export type { ConnectorConfig, SendOptions } from './LocationServiceConnector';
|
|
5
|
-
export { TokenProvider } from '../auth/TokenProvider';
|
|
6
|
-
export type { TokenProviderConfig, TokenResponse } from '../auth/TokenProvider';
|
package/dist/server/index.js
CHANGED
package/dist/types/index.d.ts
CHANGED
|
@@ -5,3 +5,30 @@ export interface ClientConfig {
|
|
|
5
5
|
* called on every request so token updates are reflected without recreating the client. */
|
|
6
6
|
getToken?: () => string | undefined;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Minimal interface for AWS SDK command objects.
|
|
10
|
+
* All AWS SDK commands (AutocompleteCommand, SearchTextCommand, etc.) extend
|
|
11
|
+
* Smithy's Command base class which has an `input` property containing the
|
|
12
|
+
* request parameters. This interface captures what we actually need from
|
|
13
|
+
* commands without coupling to Smithy internals.
|
|
14
|
+
*/
|
|
15
|
+
export interface GeoPlacesCommand {
|
|
16
|
+
readonly input: object;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Minimal interface for a MapLibre Map instance.
|
|
20
|
+
* Using a structural type avoids hard coupling to a specific maplibre-gl version.
|
|
21
|
+
*/
|
|
22
|
+
export interface MapLike {
|
|
23
|
+
isStyleLoaded(): boolean | void;
|
|
24
|
+
on(event: string, listener: (...args: unknown[]) => void): void;
|
|
25
|
+
off(event: string, listener: (...args: unknown[]) => void): void;
|
|
26
|
+
getStyle(): {
|
|
27
|
+
layers: Array<{
|
|
28
|
+
id: string;
|
|
29
|
+
type: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
getLayoutProperty(layerId: string, name: string): unknown;
|
|
33
|
+
setLayoutProperty(layerId: string, name: string, value: unknown): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round coordinate arrays in command inputs for better API cache hits.
|
|
3
|
+
*
|
|
4
|
+
* BiasPosition is rounded to 2 decimal places (~1.1 km precision) —
|
|
5
|
+
* sufficient for spatial biasing while maximizing cache reuse across
|
|
6
|
+
* nearby users.
|
|
7
|
+
*
|
|
8
|
+
* QueryPosition (reverse geocode) keeps full precision since it
|
|
9
|
+
* represents an exact point the user clicked or their device reported.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Shallow-clone the input and round position arrays that benefit from caching.
|
|
13
|
+
* Returns the original object if no position fields are present.
|
|
14
|
+
*/
|
|
15
|
+
export declare function roundPositionFields<T extends object>(input: T): T;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Round coordinate arrays in command inputs for better API cache hits.
|
|
3
|
+
*
|
|
4
|
+
* BiasPosition is rounded to 2 decimal places (~1.1 km precision) —
|
|
5
|
+
* sufficient for spatial biasing while maximizing cache reuse across
|
|
6
|
+
* nearby users.
|
|
7
|
+
*
|
|
8
|
+
* QueryPosition (reverse geocode) keeps full precision since it
|
|
9
|
+
* represents an exact point the user clicked or their device reported.
|
|
10
|
+
*/
|
|
11
|
+
const BIAS_DECIMALS = 2; // ~1.1 km grid
|
|
12
|
+
const BIAS_FACTOR = 10 ** BIAS_DECIMALS;
|
|
13
|
+
/** Known position field names and whether they should be rounded */
|
|
14
|
+
const POSITION_FIELDS = {
|
|
15
|
+
BiasPosition: true, // geocode, autocomplete, search — round for cache
|
|
16
|
+
QueryPosition: false, // reverse geocode — keep full precision
|
|
17
|
+
};
|
|
18
|
+
function roundCoord(value) {
|
|
19
|
+
return Math.round(value * BIAS_FACTOR) / BIAS_FACTOR;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Shallow-clone the input and round position arrays that benefit from caching.
|
|
23
|
+
* Returns the original object if no position fields are present.
|
|
24
|
+
*/
|
|
25
|
+
export function roundPositionFields(input) {
|
|
26
|
+
if (!input || typeof input !== 'object')
|
|
27
|
+
return input;
|
|
28
|
+
let modified = false;
|
|
29
|
+
const result = { ...input };
|
|
30
|
+
for (const [field, shouldRound] of Object.entries(POSITION_FIELDS)) {
|
|
31
|
+
const value = result[field];
|
|
32
|
+
if (!shouldRound || !Array.isArray(value) || value.length < 2)
|
|
33
|
+
continue;
|
|
34
|
+
result[field] = value.map(roundCoord);
|
|
35
|
+
modified = true;
|
|
36
|
+
}
|
|
37
|
+
return modified ? result : input;
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaosity/location-client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Client library for Chaosity Location Service with AWS Location Service compatibility",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,9 +18,14 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc",
|
|
20
20
|
"dev": "tsc --watch",
|
|
21
|
-
"
|
|
21
|
+
"lint": "eslint . && prettier --check .",
|
|
22
|
+
"lint:fix": "eslint --fix . && prettier --write .",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"test": "vitest run --passWithNoTests",
|
|
22
25
|
"test:watch": "vitest",
|
|
23
|
-
"prepublishOnly": "npm run build"
|
|
26
|
+
"prepublishOnly": "npm run build",
|
|
27
|
+
"prepare": "husky",
|
|
28
|
+
"postversion": "git push --follow-tags"
|
|
24
29
|
},
|
|
25
30
|
"keywords": [
|
|
26
31
|
"aws",
|
|
@@ -55,10 +60,17 @@
|
|
|
55
60
|
}
|
|
56
61
|
},
|
|
57
62
|
"devDependencies": {
|
|
63
|
+
"@eslint/js": "^10.0.1",
|
|
58
64
|
"@types/debug": "^4.1.12",
|
|
59
65
|
"@types/node": "^25.0.3",
|
|
66
|
+
"@vitest/coverage-v8": "^3.0.0",
|
|
67
|
+
"eslint": "^10.1.0",
|
|
68
|
+
"husky": "^9.0.0",
|
|
60
69
|
"maplibre-gl": "^5.0.0",
|
|
70
|
+
"prettier": "^3.8.1",
|
|
71
|
+
"prettier-plugin-organize-imports": "^4.3.0",
|
|
61
72
|
"typescript": "^5.0.0",
|
|
73
|
+
"typescript-eslint": "^8.57.1",
|
|
62
74
|
"vitest": "^3.0.0"
|
|
63
75
|
},
|
|
64
76
|
"publishConfig": {
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { LocationServiceConnector } from './LocationServiceConnector';
|
|
2
|
-
export interface ServerAuthConfig {
|
|
3
|
-
apiUrl?: string;
|
|
4
|
-
clientId?: string;
|
|
5
|
-
clientSecret?: string;
|
|
6
|
-
}
|
|
7
|
-
/**
|
|
8
|
-
* Get LocationServiceConnector with automatic token management.
|
|
9
|
-
*
|
|
10
|
-
* Automatically reads from environment variables:
|
|
11
|
-
* - LOCATION_API_URL or LOCATION_SERVICE_API_URL
|
|
12
|
-
* - LOCATION_CLIENT_ID or LOCATION_SERVICE_CLIENT_ID
|
|
13
|
-
* - LOCATION_CLIENT_SECRET or LOCATION_SERVICE_CLIENT_SECRET
|
|
14
|
-
*
|
|
15
|
-
* You can override any value by passing it explicitly.
|
|
16
|
-
*
|
|
17
|
-
* WARNING: This function uses client credentials (clientId/clientSecret).
|
|
18
|
-
* Only call this from:
|
|
19
|
-
* - Next.js Server Components/Actions
|
|
20
|
-
* - Node.js backend servers
|
|
21
|
-
* - API routes
|
|
22
|
-
*
|
|
23
|
-
* NEVER call from browser/client code as it exposes credentials.
|
|
24
|
-
*
|
|
25
|
-
* @example
|
|
26
|
-
* // Server Action for React provider
|
|
27
|
-
* 'use server'
|
|
28
|
-
* export async function getLocationConnector() {
|
|
29
|
-
* return getConnector()
|
|
30
|
-
* }
|
|
31
|
-
*
|
|
32
|
-
* // Or with custom config
|
|
33
|
-
* export async function getLocationConnector() {
|
|
34
|
-
* return getConnector({ apiUrl: 'https://custom.api.com' })
|
|
35
|
-
* }
|
|
36
|
-
*/
|
|
37
|
-
export declare function getConnector(config?: ServerAuthConfig): LocationServiceConnector;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { LocationServiceConnector } from './LocationServiceConnector';
|
|
2
|
-
/**
|
|
3
|
-
* Get LocationServiceConnector with automatic token management.
|
|
4
|
-
*
|
|
5
|
-
* Automatically reads from environment variables:
|
|
6
|
-
* - LOCATION_API_URL or LOCATION_SERVICE_API_URL
|
|
7
|
-
* - LOCATION_CLIENT_ID or LOCATION_SERVICE_CLIENT_ID
|
|
8
|
-
* - LOCATION_CLIENT_SECRET or LOCATION_SERVICE_CLIENT_SECRET
|
|
9
|
-
*
|
|
10
|
-
* You can override any value by passing it explicitly.
|
|
11
|
-
*
|
|
12
|
-
* WARNING: This function uses client credentials (clientId/clientSecret).
|
|
13
|
-
* Only call this from:
|
|
14
|
-
* - Next.js Server Components/Actions
|
|
15
|
-
* - Node.js backend servers
|
|
16
|
-
* - API routes
|
|
17
|
-
*
|
|
18
|
-
* NEVER call from browser/client code as it exposes credentials.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // Server Action for React provider
|
|
22
|
-
* 'use server'
|
|
23
|
-
* export async function getLocationConnector() {
|
|
24
|
-
* return getConnector()
|
|
25
|
-
* }
|
|
26
|
-
*
|
|
27
|
-
* // Or with custom config
|
|
28
|
-
* export async function getLocationConnector() {
|
|
29
|
-
* return getConnector({ apiUrl: 'https://custom.api.com' })
|
|
30
|
-
* }
|
|
31
|
-
*/
|
|
32
|
-
export function getConnector(config) {
|
|
33
|
-
return new LocationServiceConnector(config);
|
|
34
|
-
}
|