Web SDK
@atasu/maps-gl: maps, language and theme, search, routes to entrances, reports and errors in the browser.
On this page
@atasu/maps-gl is the Atasu Maps SDK for browsers, built on MapLibre GL JS 6.15.
Open source (Apache-2.0), ESM, TypeScript types included. Version 0.2.0 — see the changelog.
npm install @atasu/maps-glYou need a web key restricted to your site's origins (quick start, step 1). The samples
below that start with map. continue the quick start: map is the object AtasuMap.create() returned.
Create a map
import { AtasuMap } from '@atasu/maps-gl';
import '@atasu/maps-gl/maplibre-gl.css';
const map = await AtasuMap.create({
container: 'map', // element or its id
apiKey: 'YOUR_WEB_KEY',
language: 'ru', // 'kk' (default) | 'ru' | 'en'
theme: 'auto', // 'day' | 'night' | 'auto' (follows the system)
center: [76.95692, 43.24313],
zoom: 15,
customAttribution: '© My Company', // added next to the required attribution, never instead of it
onError: (err) => console.error('map session stopped', err), // e.g. the key was revoked
});
await map.ready(); // first style and tiles loadedcreate()first starts a map session and gets a tile token, then loads the style, so no tile is requested without a token. The token is refreshed before it expires, when a sleeping tab becomes visible again and when the browser comes back online.- Any other MapLibre
MapOptionscan be passed, exceptstyle,transformRequestandattributionControl(the SDK owns them). - Several maps on one page are fine; each has its own session. Call
map.destroy()when you remove one.
Language and theme without reloading
map.setLanguage('en'); // labels switch at once, the style is not reloaded
map.setTheme('night');
console.log(map.language, map.theme);Kazakh labels fall back to Russian and then to the local name when an object has no Kazakh name. auto follows
the user's light / dark setting.
Search
Type-ahead suggestions; the keystrokes of one search share a session token and are counted once:
const { items } = await map.search.autocomplete('Абай', { limit: 5 });
for (const s of items) console.log(s.label, s.secondary ?? '', s.id);Full search, free text or structured. House, microdistrict and entrance numbers must match exactly (12 never
matches 21):
const free = await map.search.search('Минина 24');
const structured = await map.search.search({ city: 'Алматы', street: 'Минина', house: '24' });
console.log(free.items[0]?.name, structured.items[0]?.precision);What is at a point — the address and the nearest entrance:
const { result, nearest_entrance } = await map.search.reverse([76.95692, 43.24313]);
console.log(result?.name, nearest_entrance?.ref);Place details with entrances (number, flat range, type, access):
const { items } = await map.search.search('Минина 24');
const place = await map.search.place(items[0].id);
for (const e of place.entrances) console.log(e.ref, e.flats ?? '', e.entrance_type ?? '', e.lnglat);Routes to an entrance
Waypoints are [lng, lat], { place_id } or { entrance_id }. Routing to an entrance ends the route at the
door, not at the middle of the building:
const { items } = await map.search.search('Минина 24');
const place = await map.search.place(items[0].id);
const entrance = place.entrances[0];
const route = await map.routing.route({
from: [76.95692, 43.24313],
to: entrance ? { entrance_id: entrance.id } : { place_id: place.id },
costing: 'pedestrian', // 'auto' (default) | 'taxi' | 'pedestrian'
alternatives: 1,
});
map.routes.draw(route); // the style's route layers; alternatives are dimmed
console.log(route.waypoints?.at(-1)?.resolved_to); // 'entrance'map.routes.clear() removes the route. The route, the language, the theme and your own layers survive
map.setStyle().
Your own layers
map.maplibre is the full MapLibre map. Add your layers below the labels so names stay readable:
const ml = map.maplibre;
ml.addSource('my-points', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [{ type: 'Feature', properties: {}, geometry: { type: 'Point', coordinates: [76.95692, 43.24313] } }] },
});
const firstLabel = ml.getStyle().layers.find((l) => l.id.startsWith('label-'))?.id;
ml.addLayer({ id: 'my-points', type: 'circle', source: 'my-points', paint: { 'circle-radius': 8, 'circle-color': '#e11d48' } }, firstLabel);Errors
Every call returns the typed response or throws:
AtasuApiError— the API answered with an error:statusand a stablecode(error codes). Show users your own text keyed bycode, never the Englishdetail.AtasuNetworkError— no answer (offline, DNS, CORS).AtasuDestroyedError— the map was destroyed.
import { AtasuApiError, isNotAvailable } from '@atasu/maps-gl';
try {
await map.routing.route({ from: [76.95692, 43.24313], to: [0, 0] });
} catch (err) {
if (err instanceof AtasuApiError) console.log(err.status, err.code); // 422 outside_coverage
else if (isNotAvailable(err)) console.log('Atasu Maps is not reachable right now');
else throw err;
}Reports: places, entrances, road events
Users can add places and entrances, fix or close places and report road events. These calls need a signed-in
user (userToken: an Atasuai access token) and a key with the Contributions product. Each report is sent
exactly once, also when the network drops: it waits in an outbox and is re-sent with the same idempotency key.
Every value comes from what the user entered — never copy map data into a report.
map.setUserToken(() => getAccessToken()); // your sign-in: returns the user's current access token
const sent = await map.reports.addPlace({
category: 'cafe',
names: { kk: 'Тест кафе', ru: 'Тест кафе' },
lnglat: [76.95692, 43.24313], // where the user put the pin
});
console.log(sent.status); // 'sent' (or 'queued' while offline)Submissions are reviewed by moderators before they appear on the map and in search.
Security
The SDK attaches credentials in one place only: the key goes to api.atasumaps.com, the tile token to
tiles.atasumaps.com (exact host, https), the user token only with API calls. Any other host — your own tiles,
look-alike domains — gets nothing. Server keys (atk_srv_…) are refused in the browser.
Local development against your own Atasu stack
When you run the Atasu stack locally (apps/api and its tile edge), point apiBaseUrl at it. Only
localhost, 127.0.0.1 and [::1] are accepted, and the tile token goes only to the local tile host that API
names:
const map = await AtasuMap.create({ container: 'map', apiKey: 'YOUR_WEB_KEY', apiBaseUrl: 'http://127.0.0.1:8787' });Bundlers, size
ESM only. No bundler configuration is needed: the SDK ships MapLibre's worker file and points MapLibre at it.
The SDK's own code is about 15 KB gzip; MapLibre GL JS adds about 280 KB gzip plus its worker (loaded in
parallel). If you already call MapLibre's setWorkerUrl(), the SDK keeps your setting.