# @navvo/maps-js

The **Navvo Maps JavaScript SDK** for the browser — Navvo's flagship web SDK. A self-contained GL map renderer pointed at Navvo styles, it ships a complete `NavvoServices` REST client (search, geocoding, places, routing, traffic, overlays, weather, mobility) with a typed error model, retries, and first-party TypeScript types.

- **Renderer half — `NavvoMaps`**: `Map`, `Marker`, `Popup`, controls, GeoJSON layers, live data overlays (traffic / incidents / road closures / weather), 3D helpers.
- **Services half — `NavvoServices`**: stateless, keyed REST facade for every Navvo endpoint. Runs in the browser **and** in Node (SSR) via the DOM-free `@navvo/maps-js/services` subpath.

> Full platform reference: **[docs.navvo.io](https://docs.navvo.io)**.

---

## Install

```bash
npm install @navvo/maps-js
```

The GL rendering engine installs automatically — there is nothing else to install. Or use the script-tag (UMD) build for a true drop-in:

```html
<link href="https://docs.navvo.io/navvo-maps/navvo-maps.css" rel="stylesheet" />
<script src="https://docs.navvo.io/navvo-maps/navvo-maps.js"></script>
```

## Authentication — one key, two transports

Navvo uses two credential transports and the SDK manages both from a single `apiKey`:

| Transport | Used for | How it travels |
| --- | --- | --- |
| **Header** `x-api-key: nvvo_…` | All REST service calls | set by `NavvoServices` on every request |
| **Query param** `?api_key=nvvo_…` | Style / tile / glyph / sprite fetches (browsers can't set headers) | injected by the map into the style URL + every sub-request via `transformRequest` |

> Browser keys are public — restrict them to your origin(s) (HTTP referrer allowlist) in the developer portal and minimise scopes (`tiles` + `search` + `geocode` + `places` + `routing`, plus `traffic`/`imagery` if used). Keep `geo`/`insights`/`optimize`/`telemetry` keys server-side.

## Quickstart — display a map

```ts
import { NavvoMaps } from "@navvo/maps-js";
import "@navvo/maps-js/css";

const map = new NavvoMaps.Map({
  container: "map",
  apiKey: "nvvo_YOUR_KEY",       // SDK injects ?api_key= into every map URL
  style: "standard",             // standard | positron | dark | satellite | terrain | 3d
  center: [35.9106, 31.9539],    // [lng, lat] — Amman
  zoom: 12,
});

map.addControl(new NavvoMaps.NavigationControl(), "top-right");
map.addMarker([35.9106, 31.9539]).setPopup(new NavvoMaps.Popup().setText("Amman"));
```

### Configure once (recommended)

```ts
const sdk = NavvoMaps.configure({ apiKey: "nvvo_YOUR_KEY" });
const map = sdk.map({ container: "map", style: "dark", center: [35.91, 31.95], zoom: 12 });
const services = sdk.services; // already keyed
```

---

## Map renderer (`NavvoMaps`)

### Styles & runtime switching

```ts
const { styles } = await services.styles();   // GET /tiles/styles — build a switcher
map.setStyle("dark");                          // short name → resolved URL
map.setStyle("3d", { terrain: true });         // ?style=3d&terrain=1
```

`setStyle` **preserves** your added sources, layers, and overlays across the swap.

### Markers, popups, GeoJSON

```ts
map.addMarker([35.913, 31.953], { color: "#386BFF" });
map.addPopup([35.913, 31.953], "<h3>Jordan Beans Cafe</h3><p>★ 4.6 · Open</p>");

const layer = map.addGeoJSON("my-data", featureCollection, {
  type: "fill",
  paint: { "fill-color": "#386BFF", "fill-opacity": 0.3 },
});
layer.setData(updated);
layer.remove();
```

### Live data overlays (bbox-driven, refreshable)

```ts
map.on("load", async () => {
  const traffic   = await map.addTrafficLayer();      // congestion lines (scope: traffic)
  const incidents = await map.addIncidentsLayer();    // incident pins   (scope: traffic)
  const closures  = await map.addRoadClosuresLayer(); // road edits      (scope: tiles)
  const weather   = await map.addWeatherLayer();      // city weather pins (scope: tiles)

  map.on("moveend", () => traffic.refresh());          // re-fetch for the new viewport
  // later: traffic.remove();
});
```

### 3D & terrain

```ts
const map = new NavvoMaps.Map({ container: "map", style: "3d", terrain: true, pitch: 60 });
map.enable3D({ terrain: true, exaggeration: 1.2 }); // extrusions + terrain (guarded on source presence)
map.setTerrain(true);
```

---

## Services (`NavvoServices`)

```ts
import { NavvoServices } from "@navvo/maps-js";          // or "@navvo/maps-js/services" (DOM-free)
const services = new NavvoServices({ apiKey: "nvvo_YOUR_KEY", language: "en" });
```

Config: `{ apiKey, baseUrl="https://navvo.io/api/v1", timeoutMs=15000, retry={retries:3,baseDelayMs:500,factor:2,jitter:true}, userAgent, language, throwOnUnavailable=false }`.

### Tiles & config

```ts
await services.styles();                         // GET /tiles/styles
services.styleUrl("dark");                        // /tiles/style?style=dark&api_key=…
await services.mapConfig();                       // GET /map/config (cached once)
await services.layers();                          // GET /tiles/layers
await services.categoryStyles();                  // GET /map/category-styles
```

### Search & places

```ts
const r = await services.search("coffee in Abdoun", { limit: 10, locale: "en" });
// r.items[].lngLat is parsed from WKT POINT(lng lat); r.total / r.page / r.limit

await services.place("jordan-beans-cafe");        // GET /places/{slug} — returns the place, or null on not-found (soft-null; never throws on 404)
await services.placeThumb("jordan-beans-cafe");   // GET /places/thumb (map card)
await services.placeHours("jordan-beans-cafe");   // GET /places/{slug}/hours
await services.placePhotos("jordan-beans-cafe");  // GET /places/{slug}/photos
await services.placeCategories();                 // GET /places/categories
await services.placeFacets();                     // GET /places/facets
await services.placesList({ category: "cafe", openNow: true, near: [35.91, 31.95], radiusKm: 3 });
await services.reachable({ origin: [35.91, 31.95], minutes: 15, costing: "pedestrian", categories: ["cafe"] });
```

### Geocoding (always check `available`)

```ts
const fwd = await services.geocode("Abdali Hospital, Amman", { limit: 5, bbox: map.engine.getBounds() });
if (fwd.available) console.log(fwd.results[0].displayName, fwd.results[0].lngLat);

const rev = await services.reverseGeocode([35.91, 31.95]);                 // GET /geocode/reverse?lat&lon
const batch = await services.geocodeBatch(["Amman", "Aqaba", "Irbid"]);    // POST /geocode/batch (max 50)
const rbatch = await services.reverseGeocodeBatch([[35.91, 31.95], [35, 29.5]]); // POST /geocode/reverse-batch
```

### Directions & ETA

```ts
// Corridor-aware (auto-avoids no-entry signs, incidents, closures) — POST /routing/plan
const route = await services.directions.plan({
  waypoints: [{ lngLat: [35.91, 31.95], name: "Start" }, { lngLat: [35.95, 31.99], name: "End" }],
  costing: "auto", preference: "fastest", includeLabels: false,
});
// route.geometry → decoded polyline6 [lng,lat][]; route.legs[].maneuvers; route.summary; route.alternates
const turns = services.directions.getTurnList(route); // [{ icon, text, distanceText, durationText }]

// Simple/fast (no avoidance) — POST /routing/route
await services.simpleRoute({ waypoints: [{ lngLat: [35.91, 31.95] }, { lngLat: [35.95, 31.99] }] });

// Traffic-aware ETA + congestion segments — POST /traffic/route (scopes: routing + traffic)
const eta = await services.trafficRoute({ waypoints: [{ lngLat: [35.91, 31.95] }, { lngLat: [35.95, 31.99] }] });
// eta.durationInTrafficSeconds, eta.delaySeconds, eta.segments (GeoJSON coloured by congestion)
```

### Matrix, isochrone, optimize, map-match

```ts
const m = await services.matrix({ origins: [[35.91, 31.95]], destinations: [[35.93, 31.9]], costing: "auto" });
// m.cells[i][j] = { distance, time } (null for unreachable)

const iso = await services.isochrone({ origin: [35.91, 31.95], costing: "pedestrian", contours: [{ minutes: 5 }, { minutes: 10 }] });
const opt = await services.optimize({ stops: [[35.91,31.95],[35.95,31.99],[35.97,31.88]], costing: "auto" }); // opt.order
await services.mapMatch({ shape: [{ lat: 31.95, lon: 35.91 }], costing: "auto", shape_match: "map_snap" });
```

### Traffic, incidents, road edits, route risk

```ts
await services.trafficSummary(map.engine.getBounds());   // GET /traffic/summary
await services.trafficLayers();                          // GET /traffic/layers
await services.incidents({ bbox: map.engine.getBounds() }); // GET /incidents/geojson
await services.incidentTypes();                          // GET /incidents/types
await services.reportIncident({ type: "hazard", lngLat: [35.91, 31.95], severity: 3 }); // POST /incidents/report
await services.voteIncident(id, "confirm");              // POST /incidents/{id}/votes
await services.roadEdits(map.engine.getBounds());        // GET /road-edits/geojson
await services.routeRisk(route.geometry, { timeOffset: "+1h" }); // POST /map/route-risk
await services.routeEnforcement(route.geometry);         // POST /map/route-enforcement
```

### Weather & air quality

```ts
await services.weatherPoint([35.91, 31.95], { units: "metric" }); // GET /map/weather/point (+ live AQI)
await services.weatherForecast([35.91, 31.95], { hours: 24 });    // GET /map/weather/forecast
await services.weatherAlerts([35.91, 31.95]);                     // GET /map/weather/alerts
await services.weatherCities(bbox, 9);                            // GET /map/weather/cities (zoom ≥ 7)
await services.airQualityCities(bbox, 9);                         // GET /map/airquality/cities (zoom ≥ 7)
```

### Mobility (EV / transit / GBFS)

```ts
await services.evStations({ bbox, minKw: 50, limit: 100 });       // GET /ev/stations
await services.transitStops([46.6753, 24.7136], 800);            // GET /transit/stops
await services.gbfs([55.2708, 25.2048], { radius: 600, type: "scooter" }); // GET /transit/gbfs
```

### Overlays & imagery

```ts
await services.overlay("biking", { bbox });                       // GET /map/overlays/{layerType}
await services.mapFeatures({ bbox });                             // GET /imagery/map-features
```

---

## Error handling

Every method throws typed errors and never throws for `{ available: false }` envelopes (geocode/routing) unless you set `throwOnUnavailable: true`.

```ts
import { NavvoAuthError, NavvoScopeError, NavvoRateLimitError, NavvoError } from "@navvo/maps-js";

try {
  await services.trafficRoute({ waypoints: [...] });
} catch (e) {
  if (e instanceof NavvoAuthError) /* 401 — bad/missing key */;
  else if (e instanceof NavvoScopeError) console.error("Missing scope:", e.missingScope); // 403
  else if (e instanceof NavvoRateLimitError) /* 429/quota — back off (e.retryAfterMs) */;
  else if (e instanceof NavvoError) console.error(e.status, e.code, e.message);
}
```

The HTTP client retries (exponential backoff + full jitter) on `5xx`, network/timeout, and `429`/quota — never on `400`/`401`/`403-scope`/`404`. Routing endpoints return **HTTP 201**; the SDK treats that as success.

## Coordinate conventions

The SDK's **public surface is always `[lng, lat]`**. Adapters convert per endpoint (place center WKT, bbox `west,south,east,north`, routing `{lat,lon}` vs `{lon,lat}`, reverse-geocode `lat`/`lon` params). A dev-mode guard warns on suspiciously swapped coordinates (latitude magnitude > 90).

## SSR / Next.js

The renderer is client-only; the services module is isomorphic.

```tsx
import dynamic from "next/dynamic";
const NavvoMapView = dynamic(() => import("./NavvoMapView"), { ssr: false });
```

Create the map in `useEffect`, store it in a ref, and call `map.remove()` on cleanup. Run `NavvoServices` on the server with a **server-only key** (header transport); use a separate referrer-restricted browser key for the map. Import the DOM-free services subpath on the server:

```ts
import { NavvoServices } from "@navvo/maps-js/services";
```

## Build artifacts

| File | Format |
| --- | --- |
| `dist/index.mjs` | ESM |
| `dist/index.cjs` | CommonJS |
| `dist/navvo-maps.umd.js` | UMD/IIFE global `NavvoMaps` (rendering engine bundled) |
| `dist/navvo-maps.css` | Styles (renderer chrome + Navvo controls) |
| `dist/index.d.ts`, `dist/services.d.ts` | TypeScript types |

## License

MIT © Navvo. See [LICENSE](./LICENSE).
