Atasu Maps

Web SDK

@atasu/maps-gl: maps, language and theme, search, routes to entrances, reports and errors in the browser.

@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.

Shell
npm install @atasu/maps-gl

You 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

JavaScripttested
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 loaded
  • create() 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 MapOptions can be passed, except style, transformRequest and attributionControl (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

JavaScripttested
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.

Type-ahead suggestions; the keystrokes of one search share a session token and are counted once:

JavaScripttested
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):

JavaScripttested
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:

JavaScripttested
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):

JavaScripttested
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:

JavaScripttested
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:

JavaScripttested
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: status and a stable code (error codes). Show users your own text keyed by code, never the English detail.
  • AtasuNetworkError — no answer (offline, DNS, CORS).
  • AtasuDestroyedError — the map was destroyed.
JavaScripttested
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.

JavaScripttested
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:

JavaScript
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.