@afrimap/sdk
v1.0.2
Published
AfriMap Engine SDK — West African mapping API for routing, geocoding, and POI search
Maintainers
Readme
AfriMap JavaScript / TypeScript SDK
TypeScript-first SDK for the AfriMap Engine API. Provides routing, geocoding, places search, and MapLibre GL JS map integration helpers for web applications.
Requirements
| Dependency | Minimum version | |---|---| | Node.js | 18+ | | MapLibre GL JS | 4.5+ (optional, for map rendering and 3D car markers) | | TypeScript | 5.x (optional, types included) |
Installation
npm install @afrimap/sdk
# or
yarn add @afrimap/sdkQuick start
import { AfriMapClient } from '@afrimap/sdk';
const client = new AfriMapClient({
apiKey: 'afm_live_your_key_here', // get one at portal.afrimap.ci
baseUrl: 'https://api.afrimap.ci', // default
});Routing
// Point-to-point directions (Plateau → Cocody, Abidjan)
const response = await client.routing.route({
origin: { lat: 5.3197, lng: -4.0167 },
destination: { lat: 5.3480, lng: -3.9904 },
mode: 'auto',
language: 'fr',
});
const route = response.routes[0];
console.log(`${route.distance} km, ${route.duration / 60} min`);
// eta_enhanced: ML-adjusted ETA in seconds (undefined when no traffic data)
if (route.eta_enhanced !== undefined) {
console.log(`Traffic-adjusted ETA: ${route.eta_enhanced} s`);
}
// Turn-by-turn instructions
for (const leg of route.legs) {
for (const maneuver of leg.maneuvers) {
console.log(`${maneuver.type}: ${maneuver.instruction}`);
}
}Geocoding
// Forward geocode
const result = await client.geocoding.forward('Cocody Abidjan');
console.log(result.features[0].geometry.coordinates); // [-3.9904, 5.3480]
// Reverse geocode (returns neighbourhood name when inside a known quartier)
const reverse = await client.geocoding.reverse({ lat: 5.3480, lng: -3.9904 });
console.log(reverse.features[0].properties.neighbourhood); // "Riviera 2"
// Autocomplete (merges Elasticsearch + local landmark DB)
const suggestions = await client.geocoding.autocomplete('rond-point', {
focus: { lat: 5.3614, lng: -3.9689 },
});landmark_nearby
When a result has a well-known landmark within ~200 m, the landmark_nearby property is set:
for (const feature of suggestions.features) {
const lm = feature.properties.landmark_nearby;
if (lm) {
console.log(`Near ${lm.name} (${lm.category}, ${lm.distance_m} m away)`);
}
}Places
// Nearby search with market hours
const places = await client.places.nearby({
lat: 5.36, lng: -4.03,
radius: 500,
category: 'market',
});
for (const place of places.places) {
if (place.market_hours?.open_today) {
console.log(`${place.name} open until ${place.market_hours.closes_at}`);
}
}Map Styles
AfriMap provides three production-ready MapLibre GL JS styles. All styles are:
- Mobile-optimised — larger labels at low zoom,
symbol-spacingto prevent crowding,text-optionalso icons show even when text doesn't fit, minor road labels deferred until z15 - Traffic overlay included — toggleable
trafficline layer with data-driven color (green/orange/red). Toggle withmap.setLayoutProperty('traffic', 'visibility', 'none')
| Style name | Description | Best for |
|---|---|---|
| day | "Sahel Clair" — warm sand/terracotta palette | Daytime navigation |
| night | "Lagune Nuit" — dark blue/charcoal | Night driving, OLED screens |
| satellite | Esri World Imagery + vector overlay | Aerial view, landmark identification |
Loading a style
import maplibregl from 'maplibre-gl';
// via the AfriMap SDK (automatically injects API key)
const styleUrl = client.maps.styleUrl('day'); // or 'night', 'satellite'
const map = new maplibregl.Map({
container: 'map',
style: styleUrl,
center: [-4.0167, 5.3197], // Plateau, Abidjan
zoom: 12,
});Direct style URLs (without SDK)
const API_KEY = 'afm_live_your_key_here';
const GATEWAY = 'https://api.afrimap.ci';
const map = new maplibregl.Map({
container: 'map',
style: `${GATEWAY}/v1/tiles/style/day?key=${API_KEY}`,
// Alternatively fetch and inject key:
// style: await fetch(`${GATEWAY}/v1/tiles/style/day`, {
// headers: { 'X-AfriMap-Key': API_KEY }
// }).then(r => r.json()),
center: [-4.0167, 5.3197],
zoom: 12,
});3D Buildings
The day and night styles include a buildings-3d fill-extrusion layer that renders Overture building height data as 3D geometry at zoom ≥ 15. It is toggleable:
// Toggle 3D buildings
map.setLayoutProperty('buildings-3d', 'visibility', 'none'); // flat
map.setLayoutProperty('buildings-3d', 'visibility', 'visible'); // 3DTraffic layer toggle
// The traffic layer is visible by default — hide it:
map.setLayoutProperty('traffic', 'visibility', 'none');
// Show it again:
map.setLayoutProperty('traffic', 'visibility', 'visible');
// Build a toggle button using the layer metadata:
const trafficLayer = map.getStyle().layers.find(l => l.id === 'traffic');
const legend = trafficLayer?.metadata?.['afrimap:legend'];
// { green: '> 40 km/h', orange: '15–40 km/h', red: '< 15 km/h' }Real-time traffic via WebSocket
const ws = new WebSocket(
`wss://api.afrimap.ci/v1/traffic/ws?bbox=-4.1,5.25,-3.9,5.40`,
[],
{ headers: { 'X-AfriMap-Key': API_KEY } },
);
ws.onmessage = (event) => {
const frame = JSON.parse(event.data);
// frame.type === 'traffic.update'
// frame.data → GeoJSON FeatureCollection
// frame.ts → ISO-8601 timestamp
// frame.bbox → [minLng, minLat, maxLng, maxLat]
if (frame.type === 'traffic.update') {
(map.getSource('traffic-live') as maplibregl.GeoJSONSource)
.setData(frame.data);
}
};3D Driver Markers
DriverMarker3D renders a glTF binary car model as a MapLibre model layer for navigation views. It smoothly interpolates 1 Hz GPS fixes into 60 fps motion via requestAnimationFrame, and falls back to a flat brand-coloured circle below zoom 14 where 3D models become unreadable.
Requires maplibre-gl >= 4.5 for native model layer support — the constructor throws if the supplied map doesn't expose addModel.
import maplibregl from 'maplibre-gl';
import { DriverMarker3D } from '@afrimap/sdk';
const map = new maplibregl.Map({
container: 'map',
style: client.maps.styleUrl('day'),
center: [-4.0167, 5.3197],
zoom: 16,
pitch: 60, // tilt the camera for a navigation feel
});
map.on('load', () => {
const car = new DriverMarker3D(map, {
id: 'driver-1',
position: { lat: 5.3197, lng: -4.0167 },
heading: 0, // degrees clockwise from north
variant: 'orange', // 'orange' | 'gray' | 'blue'
modelBaseUrl: 'https://api.afrimap.ci/v1/assets/models', // default
fallbackZoomThreshold: 14, // default
});
// Push GPS fixes — easing handles the 60 fps in-between frames
setInterval(() => car.update(nextFix, nextHeading), 1000);
// Swap colour to indicate state change (e.g. driver becomes available)
car.setVariant('gray');
// Tear down when the trip ends
// car.remove();
});The three colour variants map to:
orange—#F2731A— HopRide brand cargray—#8C8C95— available driverblue—#3373D9— premium tier
heading is degrees clockwise from north, matching the GPS bearing convention and Valhalla maneuver bearing_after values.
ETA (ML-Enhanced)
const eta = await client.eta.get({
origin: { lat: 5.3197, lng: -4.0167 },
destination: { lat: 5.3480, lng: -3.9904 },
});
console.log(`ETA: ${eta.duration_min} min (${eta.confidence}% confidence)`);
console.log(`Distance: ${eta.distance_km} km`);
console.log(`Speed factor applied: ${eta.speed_factor}`);Turn-by-turn navigation (Phase 3)
NavigationSession wraps the full GPS tracking loop described in
NAVIGATION.md §4:
import { NavigationSession, NavigationVoice } from '@afrimap/sdk';
const voice = new NavigationVoice({ apiConfig: client.apiConfig, language: 'fr' });
const session = new NavigationSession({
http: client.http,
destination: { lat: 5.3480, lng: -3.9904 },
language: 'fr',
listeners: {
onStateChange: (from, to) => console.log(from, '→', to),
onProgress: (p) => hud.updateETA(p.remainingDurationS),
onVoicePrompt: (p) => voice.speak(p),
onManeuverAdvance: (_from, _to, m) => hud.showManeuver(m),
onArrive: () => hud.showArrival(),
},
});
session.start(route);
navigator.geolocation.watchPosition((pos) => {
session.onLocationUpdate({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
heading: pos.coords.heading ?? undefined,
speedMps: pos.coords.speed ?? undefined,
});
});State machine: IDLE → NAVIGATING → OFF_ROUTE → REROUTING → NAVIGATING → ARRIVED.
Off-route threshold defaults to 50 m; arrival to 30 m; reroute retries
up to 3 times before parking the session in OFF_ROUTE.
Voice guidance
- French / English → Web Speech API at
rate = 0.9for in-car clarity. - Dioula / Baoulé / etc. → pre-fetched from the Edge TTS microservice
via
POST /v1/navigate/voice/batch, cached inIndexedDB. Callvoice.precacheRoute(route)once aftersession.start(...)to warm the cache before entering no-signal areas.
Offline maps (Phase 3)
const zones = await client.offline.listZones();
const manifest = await client.offline.getZoneVersion('cocody');
// Download every file of the bundle (tiles.mbtiles, routing.tar, landmarks.sqlite, audio.tar)
const outDir = '/path/in/your/OPFS/cocody';
await client.offline.downloadZone({ zoneId: 'cocody', baseDir: outDir,
onProgress: (p) => setBar(p.ratio) });See MOBILE_VALHALLA.md at the repo root for details on linking the
Valhalla native library for fully-offline routing (the SDK ships a stub
OfflineRouter that throws until the binary is wired in).
Driver feedback (Phase 3)
await client.feedback.submit({
driverId: 'drv-42',
type: 'road_closed',
location: { lat: 5.3197, lng: -4.0167 },
description: 'Accident bloquant',
metadata: { closure_radius_m: 80, expires_in_h: 2 },
});Supported types: road_closed, new_shortcut, name_correction, landmark_new.
Error handling
All methods throw an AfriMapError on API errors:
import { AfriMapError } from '@afrimap/sdk';
try {
const result = await client.routing.route({ ... });
} catch (err) {
if (err instanceof AfriMapError) {
console.error(err.code, err.message);
// err.code: 400 (bad request), 401 (invalid key), 429 (rate limit), 503 (upstream)
}
}Bundle size
The SDK ships as ESM + CJS with full tree-shaking support. Only the modules you import are included:
import { routing } from '@afrimap/sdk/routing'; // ~4 KB gzipped
import { geocoding } from '@afrimap/sdk/geocoding'; // ~3 KB gzipped
import { maps } from '@afrimap/sdk/maps'; // ~2 KB gzippedAbidjan coordinates reference
| Location | lat | lng | |---|---|---| | Plateau (Centre) | 5.3197 | -4.0167 | | Cocody | 5.3480 | -3.9904 | | Yopougon | 5.3364 | -4.0694 | | Abobo | 5.4200 | -4.0100 | | Adjamé | 5.3640 | -4.0270 | | Port-Bouët (Aéroport) | 5.2544 | -3.9262 |
