<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Goodneo-Solutions/smap-sdk/main/assets/smap-logo-dark.svg">
  <img src="https://raw.githubusercontent.com/Goodneo-Solutions/smap-sdk/main/assets/smap-logo.svg" alt="sMap" height="56">
</picture>

# sMap for React Native: `@superrouteca/smap-react-native`

A React Native map view for Superroute's sMap service, over the sMap Android and iOS SDKs: five colour schemes,
labels in any language, your points, lines and areas drawn on the GPU (clustered or not), 3D buildings and areas, and
tokens that renew themselves, standard style layers, basemap overrides, data files in common formats and layers from
other map services. The same components as [`@superrouteca/smap-react`](../react) (deck.gl excepted): `<SMap>`,
`<Points>`, `<Lines>`, `<Polygons>`, `<Source>`, `<StyleLayer>`, `<RasterLayer>`, `<WmsLayer>`, `<WmtsLayer>`,
`<VectorTiles>`, `<ArcGisLayer>`, `<Data>`, `<Layers>`, `useSMap()`, `useRasterUrl()` and `formats.read`.

- Version 1.1.0, MIT licence (see `LICENSE` at the repository root)
- React Native 0.81 or later with the New Architecture (a Fabric component); built and tested with 0.88
- Android 7.0 (API 24) and later, iOS 15.1 and later

## Installation

```sh
npm install @superrouteca/smap-react-native
```

**Android.** The sMap Android SDK comes from JitPack (`com.github.Goodneo-Solutions:smap-sdk:1.1.0`); the package
adds the JitPack repository itself. Set `smapAndroidVersion` in the root `ext { }` of `android/build.gradle` to use
another release tag.

**iOS.** The sMap iOS SDK is a Swift package, added to the pod by CocoaPods. Add two lines to `ios/Podfile`, then run
`pod install`:

```ruby
require Pod::Executable.execute_command('node', ['-p',
  'require.resolve("@superrouteca/smap-react-native/ios/smap_react_native.rb", {paths: [process.argv[1]]})',
  __dir__]).strip

# ... inside the target:
  post_install do |installer|
    react_native_post_install(installer, config[:reactNativePath], :mac_catalyst_enabled => false)
    SMapReactNative.post_install(installer)   # embeds the map framework in the app
  end
```

## A minimal map

```tsx
import { useRef } from 'react';
import { SMap, Points, Lines, Polygons, type SMapHandle } from '@superrouteca/smap-react-native';

export function Deliveries({ stops, route, zones }) {
  const map = useRef<SMapHandle>(null);
  return (
    <SMap ref={map} apiKey="smk_…" style="light" language="en" center={[-79.38, 43.65]} zoom={11}
      containerStyle={{ flex: 1 }} onReady={(m) => m.fitLayer('stops')}>
      <Points id="stops" data={stops} options={{ cluster: true, label: 'name' }}
        onTap={(properties) => console.log(properties.name)} />
      <Lines id="route" data={route} options={{ color: '#16a34a', width: 5 }} />
      <Polygons id="zones" data={zones} options={{ fill: '#f97316', extrude: { height: 'height' } }} />
    </SMap>
  );
}
```

### `<SMap>` props

| Prop | |
|---|---|
| `apiKey`, `token`, `getToken` | access, see below |
| `style` | `light`, `white`, `grayscale`, `dark`, `black` or a saved custom style `custom:sms_…`; a change keeps the layers (a style object here is taken as `containerStyle`) |
| `basemap` | basemap overrides by class (see below); a new value replaces the previous ones, removing the prop clears them |
| `language` | label language (`en`, `fr`, `zh-Hans`, …), the device language by default; a change keeps the layers |
| `center`, `zoom`, `bearing`, `pitch` | the camera; it follows a change |
| `buildings3D` | buildings in 3D |
| `nodes`, `baseUrl`, `nearest` | the service (default `https://tile.srsr.ca`) |
| `containerStyle` | the layout of the map view (default `flex: 1`) |
| `onReady(map)`, `onError(error)` | |

`apiKey`, `token`, `getToken`, `baseUrl`, `nodes` and `nearest` are read when the map is created; changing one creates
the map again (its layers are added again).

The ref (and `useSMap()` inside `<SMap>`) is the map once it is ready, null before: `addPoints`, `addLines`,
`addPolygons`, `setData`, `setVisible`, `removeLayer`, `layers()`, `fitLayer(id, {padding, maxZoom})`,
`flyTo(center, zoom?)`, `onTap(id, handler)`, `setStyle`, `setLanguage`, `setBuildings3D`, and the calls of 1.1.0:
`addSource`, `removeSource`, `addStyleLayer`, `setPaint`, `setLayout`, `setFilter`, `getStyleLayer`, `setBasemap`,
`basemap()`, `addData`, `toGeoJSON`, `toKML`, `convert`, `addRasterLayer`, `addWmsLayer`, `addWmtsLayer`,
`addVectorTiles`, `addArcGisLayer`, `toJSON`, `loadLayers` and `rasterUrl`. These return promises: a call without a
result resolves once the native map has made it (a failure goes to `onError`), a call with a result rejects when it
fails.

### Layers

`<Points>`, `<Lines>` and `<Polygons>` take `id`, `data` (GeoJSON, or an array of `{lng, lat, …properties}`),
`options`, `onTap(properties, {lat, lng})`, `visible` and `slot` (`bottom`, `roads`, `labels` or `top`: where the
layer draws among the basemap's layers; without one it draws on top, in the order added, as in 1.0); they are added
when the map is ready and removed on unmount.

- Points: `cluster` (`true` or `{radius, maxZoom}`), `color`, `radius`, `strokeColor`, `icon` (an image registered
  with the native map), `iconSize`, `label` (a property), `minZoom`, `maxZoom`, `zIndex`
- Lines: `color`, `width`, `dash`, `opacity`, `minZoom`, `maxZoom`, `zIndex`
- Polygons: `fill`, `opacity`, `outline`, `outlineWidth`, `extrude` (`{height, base}`: numbers or property names),
  `minZoom`, `maxZoom`, `zIndex`

Higher `zIndex` draws on top, then the order added.

## Basemap, style layers, external layers and data files

These work as in [`@superrouteca/smap-react`](../react) (see its README for every prop), drawn by the native map:

```tsx
import { SMap, Source, StyleLayer, WmsLayer, Data, formats } from '@superrouteca/smap-react-native';

const parcels = { type: 'vector', tiles: ['https://tiles.example/{z}/{x}/{y}.pbf'] };

<SMap apiKey="smk_…" style="custom:sms_…" basemap={{ water: { color: '#9cc0e6' }, pois: { visible: false } }}>
  <Source id="parcels" source={parcels}>
    <StyleLayer slot="roads" layer={{ id: 'parcel-lines', type: 'line', 'source-layer': 'parcels',
      paint: { 'line-color': '#64748b' } }} />
  </Source>
  <WmsLayer id="flood" url="https://maps.example/wms" layers="flood_zones" transparent slot="labels" />
  <Data id="trip" data={gpxText} format="gpx" onTap={(p) => console.log(p.name)} />
</SMap>
```

- `basemap`: overrides by class (`water`, `land`, `parks`, `roads` and `roads.*`, `rail`, `buildings`, `boundaries`,
  `pois`, `labels` and `labels.*`, `transit`), each with `visible`, `color`, `casing`, `halo`, `opacity`, `width`.
- `<Source>` / `<StyleLayer>`: Style Specification v8 sources and layers, passed through unchanged; a change of
  `paint`, `layout` or `filter` is set in place.
- `<RasterLayer>`, `<WmsLayer>`, `<WmtsLayer>`, `<VectorTiles>`, `<ArcGisLayer>`: tiles from other services, with the
  attribution each names.
- `<Data>`: KML, KMZ, GPX, encoded polylines, WKT, WKB, CSV, TopoJSON and GeoJSON, read on the device by the native
  readers. `data` is text, bytes (`ArrayBuffer`, `Uint8Array` or `{ base64 }`, for KMZ and WKB) or a parsed object.
  Points become the layer `<id>`, lines `<id>:lines`, areas `<id>:areas`; `onLoad` and `onError` report the reading.
- `formats.read(input, { format, lat, lng })` gives the GeoJSON FeatureCollection without a map.
- `<Layers json>` loads a layer description written by `toJSON()` of any sMap SDK, so a map built on the web opens the
  same in the app.
- `useRasterUrl(style, { lang, retina })` gives a raster tile URL template.

deck.gl layers are web only and are not offered here.

## Map keys and tokens

- **Map key** (`apiKey="smk_…"`): created in the Superroute developer center with the apps it may be used from
  (Android: package name and signing certificate; iOS: bundle id). Safe to ship inside the app. The SDK sends the
  app's identity, read from the system, with every request to the map service.
- **Token** (`token`, or `getToken={async () => …}`): signed by your own server for a limited time; with `getToken` a
  new token is asked for two minutes before the current one expires.
- With both, both are sent and the token is checked first.

The identity reaches the map's own requests through an app-wide hook of the native map; an app that sets its own
HTTP client (Android) or network delegate (iOS) for the map afterwards replaces it, and those requests then go
without the identity.

## Data attribution

The map data credit stays visible on every map, as the data licence requires. Every map shows a small sMap badge in
its bottom-right corner, beside the attribution button (left of it on iOS; on Android the button keeps the bottom-left
corner).

## Example and tests

```sh
npm install && npm test                 # the JS layer
cd example && npm install
npx react-native run-android            # or: cd ios && pod install && cd .. && npx react-native run-ios
```

The example builds against the Android and iOS SDKs in this repository (`example/android/settings.gradle` includes
`../../android/smap`; the podspec uses `../ios` when it exists, or `SMAP_IOS_PACKAGE_PATH`). Put a map key that allows
`ca.superroute.smap.rnexample` in `SMAP_KEY` in `example/App.tsx`.
