npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@afrimap/sdk

v1.0.2

Published

AfriMap Engine SDK — West African mapping API for routing, geocoding, and POI search

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/sdk

Quick 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-spacing to prevent crowding, text-optional so icons show even when text doesn't fit, minor road labels deferred until z15
  • Traffic overlay included — toggleable traffic line layer with data-driven color (green/orange/red). Toggle with map.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'); // 3D

Traffic 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 car
  • gray#8C8C95 — available driver
  • blue#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.9 for in-car clarity.
  • Dioula / Baoulé / etc. → pre-fetched from the Edge TTS microservice via POST /v1/navigate/voice/batch, cached in IndexedDB. Call voice.precacheRoute(route) once after session.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 gzipped

Abidjan 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 |