@superrouteca/smap-react-native 0.0.0-stage → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ ## 1.0.1 — 2026-10-27
4
+
5
+ - Every map shows a small sMap badge (the sMap icon and name on a semi-transparent pill that reads on all five colour
6
+ schemes) in its bottom-right corner, beside the attribution button (left of it on iOS; on Android the button keeps
7
+ the bottom-left corner). It is always shown and is not a link; the map data attribution stays visible. It comes from
8
+ the sMap Android and iOS SDKs 1.0.1, now the default.
9
+ - The package description starts with "sMap"; the README starts with the sMap logo.
10
+
11
+ ## 1.0.0 — 2026-10-27
12
+
13
+ - `<SMap>` (a New Architecture native view over the sMap Android and iOS SDKs) with access, style, language, camera and
14
+ 3D building props; the ref and `useSMap()` give the map.
15
+ - `<Points>`, `<Lines>` and `<Polygons>` added, updated and removed with the components, with `onTap`.
16
+ - Tokens from `getToken`, renewed before they expire; map keys (`apiKey`).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Goodneo Solutions
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,124 @@
1
- # Temporary Holding Version
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/Goodneo-Solutions/smap-sdk/main/assets/smap-logo-dark.svg">
3
+ <img src="https://raw.githubusercontent.com/Goodneo-Solutions/smap-sdk/main/assets/smap-logo.svg" alt="sMap" height="56">
4
+ </picture>
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # sMap for React Native: `@superrouteca/smap-react-native`
7
+
8
+ A React Native map view for Superroute's sMap service, over the sMap Android and iOS SDKs: five colour schemes,
9
+ labels in any language, your points, lines and areas drawn on the GPU (clustered or not), 3D buildings and areas, and
10
+ tokens that renew themselves. The same components as [`@superrouteca/smap-react`](../react): `<SMap>`, `<Points>`,
11
+ `<Lines>`, `<Polygons>`, `useSMap()`.
12
+
13
+ - Version 1.0.1, MIT licence (see `LICENSE` at the repository root)
14
+ - React Native 0.81 or later with the New Architecture (a Fabric component); built and tested with 0.88
15
+ - Android 7.0 (API 24) and later, iOS 15.1 and later
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ npm install @superrouteca/smap-react-native
21
+ ```
22
+
23
+ **Android.** The sMap Android SDK comes from JitPack (`com.github.Goodneo-Solutions:smap-sdk:1.0.1`); the package
24
+ adds the JitPack repository itself. Set `smapAndroidVersion` in the root `ext { }` of `android/build.gradle` to use
25
+ another release tag.
26
+
27
+ **iOS.** The sMap iOS SDK is a Swift package, added to the pod by CocoaPods. Add two lines to `ios/Podfile`, then run
28
+ `pod install`:
29
+
30
+ ```ruby
31
+ require Pod::Executable.execute_command('node', ['-p',
32
+ 'require.resolve("@superrouteca/smap-react-native/ios/smap_react_native.rb", {paths: [process.argv[1]]})',
33
+ __dir__]).strip
34
+
35
+ # ... inside the target:
36
+ post_install do |installer|
37
+ react_native_post_install(installer, config[:reactNativePath], :mac_catalyst_enabled => false)
38
+ SMapReactNative.post_install(installer) # embeds the map framework in the app
39
+ end
40
+ ```
41
+
42
+ ## A minimal map
43
+
44
+ ```tsx
45
+ import { useRef } from 'react';
46
+ import { SMap, Points, Lines, Polygons, type SMapHandle } from '@superrouteca/smap-react-native';
47
+
48
+ export function Deliveries({ stops, route, zones }) {
49
+ const map = useRef<SMapHandle>(null);
50
+ return (
51
+ <SMap ref={map} apiKey="smk_…" style="light" language="en" center={[-79.38, 43.65]} zoom={11}
52
+ containerStyle={{ flex: 1 }} onReady={(m) => m.fitLayer('stops')}>
53
+ <Points id="stops" data={stops} options={{ cluster: true, label: 'name' }}
54
+ onTap={(properties) => console.log(properties.name)} />
55
+ <Lines id="route" data={route} options={{ color: '#16a34a', width: 5 }} />
56
+ <Polygons id="zones" data={zones} options={{ fill: '#f97316', extrude: { height: 'height' } }} />
57
+ </SMap>
58
+ );
59
+ }
60
+ ```
61
+
62
+ ### `<SMap>` props
63
+
64
+ | Prop | |
65
+ |---|---|
66
+ | `apiKey`, `token`, `getToken` | access, see below |
67
+ | `style` | `light`, `white`, `grayscale`, `dark`, `black`; a change keeps the layers (a style object here is taken as `containerStyle`) |
68
+ | `language` | label language (`en`, `fr`, `zh-Hans`, …), the device language by default; a change keeps the layers |
69
+ | `center`, `zoom`, `bearing`, `pitch` | the camera; it follows a change |
70
+ | `buildings3D` | buildings in 3D |
71
+ | `nodes`, `baseUrl`, `nearest` | the service (default `https://tile.srsr.ca`) |
72
+ | `containerStyle` | the layout of the map view (default `flex: 1`) |
73
+ | `onReady(map)`, `onError(error)` | |
74
+
75
+ `apiKey`, `token`, `getToken`, `baseUrl`, `nodes` and `nearest` are read when the map is created; changing one creates
76
+ the map again (its layers are added again).
77
+
78
+ The ref (and `useSMap()` inside `<SMap>`) is the map once it is ready, null before: `addPoints`, `addLines`,
79
+ `addPolygons`, `setData`, `setVisible`, `removeLayer`, `layers()`, `fitLayer(id, {padding, maxZoom})`,
80
+ `flyTo(center, zoom?)`, `onTap(id, handler)`, `setStyle`, `setLanguage`, `setBuildings3D`.
81
+
82
+ ### Layers
83
+
84
+ `<Points>`, `<Lines>` and `<Polygons>` take `id`, `data` (GeoJSON, or an array of `{lng, lat, …properties}`),
85
+ `options`, `onTap(properties, {lat, lng})` and `visible`; they are added when the map is ready and removed on unmount.
86
+
87
+ - Points: `cluster` (`true` or `{radius, maxZoom}`), `color`, `radius`, `strokeColor`, `icon` (an image registered
88
+ with the native map), `iconSize`, `label` (a property), `minZoom`, `maxZoom`, `zIndex`
89
+ - Lines: `color`, `width`, `dash`, `opacity`, `minZoom`, `maxZoom`, `zIndex`
90
+ - Polygons: `fill`, `opacity`, `outline`, `outlineWidth`, `extrude` (`{height, base}`: numbers or property names),
91
+ `minZoom`, `maxZoom`, `zIndex`
92
+
93
+ Higher `zIndex` draws on top, then the order added.
94
+
95
+ ## Map keys and tokens
96
+
97
+ - **Map key** (`apiKey="smk_…"`): created in the Superroute developer center with the apps it may be used from
98
+ (Android: package name and signing certificate; iOS: bundle id). Safe to ship inside the app. The SDK sends the
99
+ app's identity, read from the system, with every request to the map service.
100
+ - **Token** (`token`, or `getToken={async () => …}`): signed by your own server for a limited time; with `getToken` a
101
+ new token is asked for two minutes before the current one expires.
102
+ - With both, both are sent and the token is checked first.
103
+
104
+ The identity reaches the map's own requests through an app-wide hook of the native map; an app that sets its own
105
+ HTTP client (Android) or network delegate (iOS) for the map afterwards replaces it, and those requests then go
106
+ without the identity.
107
+
108
+ ## Data attribution
109
+
110
+ The map data credit stays visible on every map, as the data licence requires. Every map shows a small sMap badge in
111
+ its bottom-right corner, beside the attribution button (left of it on iOS; on Android the button keeps the bottom-left
112
+ corner).
113
+
114
+ ## Example and tests
115
+
116
+ ```sh
117
+ npm install && npm test # the JS layer
118
+ cd example && npm install
119
+ npx react-native run-android # or: cd ios && pod install && cd .. && npx react-native run-ios
120
+ ```
121
+
122
+ The example builds against the Android and iOS SDKs in this repository (`example/android/settings.gradle` includes
123
+ `../../android/smap`; the podspec uses `../ios` when it exists, or `SMAP_IOS_PACKAGE_PATH`). Put a map key that allows
124
+ `ca.superroute.smap.rnexample` in `SMAP_KEY` in `example/App.tsx`.
@@ -0,0 +1,23 @@
1
+ require "json"
2
+ require_relative "ios/smap_react_native"
3
+
4
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
5
+
6
+ Pod::Spec.new do |s|
7
+ s.name = "SMapReactNative"
8
+ s.version = package["version"]
9
+ s.summary = package["description"]
10
+ s.homepage = package["homepage"]
11
+ s.license = { :type => package["license"] }
12
+ s.authors = package["author"]
13
+ s.platforms = { :ios => "15.1" }
14
+ s.source = { :git => "https://github.com/Goodneo-Solutions/smap-sdk.git", :tag => s.version.to_s }
15
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
16
+ s.swift_version = "5.9"
17
+ s.pod_target_xcconfig = { "DEFINES_MODULE" => "YES" }
18
+
19
+ install_modules_dependencies(s)
20
+
21
+ # The sMap iOS SDK (Swift package); see ios/smap_react_native.rb for which copy and for the app's Podfile line.
22
+ SMapReactNative.add_to_pod(s)
23
+ end
@@ -0,0 +1,46 @@
1
+ // The React Native module over the sMap Android SDK (ca.superroute.smap).
2
+ //
3
+ // The SDK comes from JitPack (com.github.Goodneo-Solutions:smap-sdk:<version>, the tag of this release) unless
4
+ // the app includes the SDK's source as the project ':smap-android' (the example does, so it builds against the
5
+ // SDK in this repository). Set `smapAndroidVersion` in the app's root `ext` to pick another tag.
6
+ def safeExtGet(prop, fallback) {
7
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
8
+ }
9
+
10
+ apply plugin: "com.android.library"
11
+ apply plugin: "org.jetbrains.kotlin.android"
12
+ apply plugin: "com.facebook.react"
13
+
14
+ android {
15
+ namespace "ca.superroute.smap.reactnative"
16
+ compileSdk safeExtGet("compileSdkVersion", 36)
17
+
18
+ defaultConfig {
19
+ minSdk safeExtGet("minSdkVersion", 24)
20
+ }
21
+
22
+ compileOptions {
23
+ sourceCompatibility JavaVersion.VERSION_17
24
+ targetCompatibility JavaVersion.VERSION_17
25
+ }
26
+ }
27
+
28
+ kotlin {
29
+ jvmToolchain(17)
30
+ }
31
+
32
+ repositories {
33
+ google()
34
+ mavenCentral()
35
+ maven { url "https://jitpack.io" }
36
+ }
37
+
38
+ dependencies {
39
+ implementation "com.facebook.react:react-android"
40
+ def localSdk = rootProject.findProject(":smap-android")
41
+ if (localSdk != null) {
42
+ api localSdk
43
+ } else {
44
+ api "com.github.Goodneo-Solutions:smap-sdk:${safeExtGet("smapAndroidVersion", "1.0.1")}"
45
+ }
46
+ }
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.INTERNET" />
3
+ </manifest>
@@ -0,0 +1,17 @@
1
+ package ca.superroute.smap.reactnative
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfoProvider
7
+ import com.facebook.react.uimanager.ViewManager
8
+
9
+ /** Registers the map view with React Native (found by autolinking). */
10
+ class SMapPackage : BaseReactPackage() {
11
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
12
+ listOf(SMapViewManager())
13
+
14
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = null
15
+
16
+ override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { emptyMap() }
17
+ }
@@ -0,0 +1,314 @@
1
+ package ca.superroute.smap.reactnative
2
+
3
+ import android.net.Uri
4
+ import android.widget.FrameLayout
5
+ import ca.superroute.smap.SMap
6
+ import ca.superroute.smap.SMapCluster
7
+ import ca.superroute.smap.SMapExtrude
8
+ import ca.superroute.smap.SMapFlavor
9
+ import ca.superroute.smap.SMapGeoJson
10
+ import ca.superroute.smap.SMapLineOptions
11
+ import ca.superroute.smap.SMapOptions
12
+ import ca.superroute.smap.SMapPointOptions
13
+ import ca.superroute.smap.SMapPolygonOptions
14
+ import ca.superroute.smap.SMapValue
15
+ import com.facebook.react.bridge.Arguments
16
+ import com.facebook.react.bridge.LifecycleEventListener
17
+ import com.facebook.react.bridge.WritableMap
18
+ import com.facebook.react.uimanager.ThemedReactContext
19
+ import com.facebook.react.uimanager.UIManagerHelper
20
+ import com.facebook.react.uimanager.events.Event
21
+ import kotlinx.coroutines.CompletableDeferred
22
+ import org.json.JSONArray
23
+ import org.json.JSONObject
24
+ import org.maplibre.android.camera.CameraPosition
25
+ import org.maplibre.android.camera.CameraUpdateFactory
26
+ import org.maplibre.android.geometry.LatLng
27
+
28
+ /**
29
+ * The view behind `<SMap>`: holds one [SMap], created when the first props arrive (the `config` prop is read then,
30
+ * and a new config arrives as a new view). Props switch the style, the language, the camera and 3D buildings;
31
+ * commands add and change layers; events report readiness, errors, taps and token requests.
32
+ */
33
+ class SMapReactView(private val reactContext: ThemedReactContext) : FrameLayout(reactContext), LifecycleEventListener {
34
+ var config: String? = null
35
+ private var flavor: String? = null
36
+ private var language: String? = null
37
+ private var camera: String? = null
38
+ private var buildings = false
39
+
40
+ private var map: SMap? = null
41
+ private var appliedFlavor: String? = null
42
+ private var appliedLanguage: String? = null
43
+ private var appliedCamera: String? = null
44
+ private var appliedBuildings = false
45
+ private var destroyed = false
46
+ private var nextTokenRequest = 0
47
+ private val tokenRequests = HashMap<Int, CompletableDeferred<String>>()
48
+
49
+ init {
50
+ reactContext.addLifecycleEventListener(this)
51
+ }
52
+
53
+ // ------------------------------------------------------------------ props
54
+
55
+ fun setFlavor(value: String?) { flavor = value }
56
+ fun setLanguageTag(value: String?) { language = value }
57
+ fun setCamera(value: String?) { camera = value }
58
+ fun setBuildings(value: Boolean) { buildings = value }
59
+
60
+ /** Called once all props of an update are set: creates the map on the first update, then applies changes. */
61
+ fun commitProps() {
62
+ if (destroyed) return
63
+ val m = map ?: run { create(); return }
64
+ if (flavor != appliedFlavor) {
65
+ appliedFlavor = flavor
66
+ m.setStyle(SMapFlavor.fromId(flavor))
67
+ }
68
+ val tag = language?.takeIf { it.isNotEmpty() }
69
+ if (language != appliedLanguage) {
70
+ appliedLanguage = language
71
+ if (tag != null) m.setLanguage(tag)
72
+ }
73
+ if (camera != appliedCamera) {
74
+ appliedCamera = camera
75
+ moveCamera(m, camera)
76
+ }
77
+ if (buildings != appliedBuildings) {
78
+ appliedBuildings = buildings
79
+ m.setBuildings3D(buildings)
80
+ }
81
+ }
82
+
83
+ private fun create() {
84
+ val c = try { JSONObject(config ?: "{}") } catch (e: Exception) { JSONObject() }
85
+ val cam = parseObject(camera)
86
+ val wantsToken = c.optBoolean("getToken", false)
87
+ val options = SMapOptions(
88
+ baseUrl = c.optStringOrNull("baseUrl") ?: SMapOptions.DEFAULT_BASE_URL,
89
+ nodes = c.optJSONArray("nodes")?.let { a -> (0 until a.length()).map { a.getString(it) } } ?: emptyList(),
90
+ token = c.optStringOrNull("token"),
91
+ getToken = if (wantsToken) ({ requestToken() }) else null,
92
+ apiKey = c.optStringOrNull("apiKey"),
93
+ nearest = c.optBoolean("nearest", true),
94
+ style = SMapFlavor.fromId(flavor),
95
+ language = language?.takeIf { it.isNotEmpty() },
96
+ center = if (cam.has("lat") && cam.has("lng")) LatLng(cam.getDouble("lat"), cam.getDouble("lng")) else null,
97
+ zoom = cam.optDoubleOrNull("zoom"),
98
+ bearing = cam.optDoubleOrNull("bearing"),
99
+ pitch = cam.optDoubleOrNull("pitch"),
100
+ onError = { e -> emit("mapError") { putString("message", e.message ?: e.toString()) } },
101
+ )
102
+ appliedFlavor = flavor
103
+ appliedLanguage = language
104
+ appliedCamera = camera
105
+ appliedBuildings = buildings
106
+ val m = SMap(context, options)
107
+ map = m
108
+ addView(m, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
109
+ m.onCreate(null)
110
+ m.onStart()
111
+ m.onResume()
112
+ if (buildings) m.setBuildings3D(true)
113
+ m.ready { ready ->
114
+ if (destroyed) return@ready
115
+ val node = Uri.parse(ready.styleUrl).let { "${it.scheme}://${it.authority}" }
116
+ emit("mapReady") { putString("node", node) }
117
+ }
118
+ }
119
+
120
+ private fun moveCamera(m: SMap, json: String?) {
121
+ val engine = m.engine ?: return
122
+ val cam = parseObject(json)
123
+ val b = CameraPosition.Builder(engine.cameraPosition)
124
+ if (cam.has("lat") && cam.has("lng")) b.target(LatLng(cam.getDouble("lat"), cam.getDouble("lng")))
125
+ cam.optDoubleOrNull("zoom")?.let { b.zoom(it) }
126
+ cam.optDoubleOrNull("bearing")?.let { b.bearing(it) }
127
+ cam.optDoubleOrNull("pitch")?.let { b.tilt(it) }
128
+ engine.moveCamera(CameraUpdateFactory.newCameraPosition(b.build()))
129
+ }
130
+
131
+ // ------------------------------------------------------------------ commands
132
+
133
+ fun addLayer(kind: String, id: String, data: String, options: String) {
134
+ val m = map ?: return
135
+ val o = parseObject(options)
136
+ try {
137
+ val fc = SMapGeoJson.toFeatureCollection(data)
138
+ when (kind) {
139
+ "points" -> m.addPoints(id, fc, pointOptions(o))
140
+ "lines" -> m.addLines(id, fc, lineOptions(o))
141
+ "polygons" -> m.addPolygons(id, fc, polygonOptions(o))
142
+ else -> throw IllegalArgumentException("sMap: unknown layer kind $kind")
143
+ }
144
+ m.onTap(id) { properties, latLng ->
145
+ emit("layerTap") {
146
+ putString("layerId", id)
147
+ putString("properties", JSONObject(jsonSafe(properties)).toString())
148
+ putDouble("lat", latLng.latitude)
149
+ putDouble("lng", latLng.longitude)
150
+ }
151
+ }
152
+ } catch (e: Exception) {
153
+ emit("mapError") { putString("message", e.message ?: e.toString()) }
154
+ }
155
+ }
156
+
157
+ fun setLayerData(id: String, data: String) {
158
+ try {
159
+ map?.setData(id, data)
160
+ } catch (e: Exception) {
161
+ emit("mapError") { putString("message", e.message ?: e.toString()) }
162
+ }
163
+ }
164
+
165
+ fun setLayerVisible(id: String, visible: Boolean) { map?.setVisible(id, visible) }
166
+
167
+ fun removeLayer(id: String) { map?.removeLayer(id) }
168
+
169
+ fun fitLayer(id: String, padding: Double, maxZoom: Double) { map?.fitLayer(id, padding.toInt(), maxZoom) }
170
+
171
+ fun flyTo(lat: Double, lng: Double, zoom: Double) { map?.flyTo(LatLng(lat, lng), if (zoom < 0) null else zoom) }
172
+
173
+ fun resolveToken(requestId: Int, token: String, error: String) {
174
+ val d = tokenRequests.remove(requestId) ?: return
175
+ if (token.isNotEmpty()) d.complete(token) else d.completeExceptionally(IllegalStateException(error.ifEmpty { "sMap: no token" }))
176
+ }
177
+
178
+ private suspend fun requestToken(): String {
179
+ val id = ++nextTokenRequest
180
+ val d = CompletableDeferred<String>()
181
+ tokenRequests[id] = d
182
+ emit("tokenRequest") { putInt("requestId", id) }
183
+ return d.await()
184
+ }
185
+
186
+ // ------------------------------------------------------------------ lifecycle and layout
187
+
188
+ override fun onHostResume() { map?.onResume() }
189
+ override fun onHostPause() { map?.onPause() }
190
+ override fun onHostDestroy() { destroy() }
191
+
192
+ fun destroy() {
193
+ if (destroyed) return
194
+ destroyed = true
195
+ reactContext.removeLifecycleEventListener(this)
196
+ tokenRequests.values.forEach { it.cancel() }
197
+ tokenRequests.clear()
198
+ map?.let {
199
+ it.onPause()
200
+ it.onStop()
201
+ it.onDestroy()
202
+ removeView(it)
203
+ }
204
+ map = null
205
+ }
206
+
207
+ // React Native lays out its own views only; the map's native views are measured and placed here.
208
+ // Nullable: requestLayout() is also called by the super constructor, before this field is set.
209
+ private val measureAndLayout: Runnable? = Runnable {
210
+ measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY))
211
+ layout(left, top, right, bottom)
212
+ }
213
+
214
+ override fun requestLayout() {
215
+ super.requestLayout()
216
+ measureAndLayout?.let { post(it) }
217
+ }
218
+
219
+ // ------------------------------------------------------------------ events
220
+
221
+ private fun emit(name: String, fill: WritableMap.() -> Unit) {
222
+ val data = Arguments.createMap().apply(fill)
223
+ val surfaceId = UIManagerHelper.getSurfaceId(this)
224
+ UIManagerHelper.getEventDispatcher(reactContext)?.dispatchEvent(SMapEvent(surfaceId, id, name, data))
225
+ }
226
+
227
+ private class SMapEvent(surfaceId: Int, viewTag: Int, private val name: String, private val data: WritableMap) :
228
+ Event<SMapEvent>(surfaceId, viewTag) {
229
+ override fun getEventName(): String = "top" + name.replaceFirstChar(Char::uppercaseChar)
230
+ override fun getEventData(): WritableMap = data
231
+ }
232
+
233
+ companion object {
234
+ val EVENTS = listOf("mapReady", "mapError", "layerTap", "tokenRequest")
235
+
236
+ private fun parseObject(json: String?): JSONObject =
237
+ try { if (json.isNullOrEmpty()) JSONObject() else JSONObject(json) } catch (e: Exception) { JSONObject() }
238
+
239
+ private fun JSONObject.optStringOrNull(name: String): String? =
240
+ if (has(name) && !isNull(name)) optString(name).takeIf { it.isNotEmpty() } else null
241
+
242
+ private fun JSONObject.optDoubleOrNull(name: String): Double? =
243
+ if (has(name) && !isNull(name)) optDouble(name).takeIf { !it.isNaN() } else null
244
+
245
+ private fun JSONObject.optFloat(name: String, alt: String? = null): Float? =
246
+ (optDoubleOrNull(name) ?: alt?.let { optDoubleOrNull(it) })?.toFloat()
247
+
248
+ private fun value(o: JSONObject, name: String, fallback: Double): SMapValue = when {
249
+ !o.has(name) || o.isNull(name) -> SMapValue.Fixed(fallback)
250
+ o.get(name) is String -> SMapValue.Property(o.getString(name))
251
+ else -> SMapValue.Fixed(o.optDouble(name, fallback))
252
+ }
253
+
254
+ /** Point options from JSON; names as in every sMap SDK (`minzoom` / `maxzoom` as in the Web SDK too). */
255
+ fun pointOptions(o: JSONObject): SMapPointOptions {
256
+ val d = SMapPointOptions()
257
+ val cluster = when (val c = o.opt("cluster")) {
258
+ is JSONObject -> SMapCluster(radius = c.optInt("radius", 50), maxZoom = c.optInt("maxZoom", 14))
259
+ true -> SMapCluster()
260
+ else -> null
261
+ }
262
+ return SMapPointOptions(
263
+ cluster = cluster,
264
+ color = o.optStringOrNull("color") ?: d.color,
265
+ radius = o.optFloat("radius") ?: d.radius,
266
+ strokeColor = o.optStringOrNull("strokeColor") ?: d.strokeColor,
267
+ icon = o.optStringOrNull("icon"),
268
+ iconSize = o.optFloat("iconSize") ?: d.iconSize,
269
+ label = o.optStringOrNull("label"),
270
+ minZoom = o.optFloat("minZoom", "minzoom"),
271
+ maxZoom = o.optFloat("maxZoom", "maxzoom"),
272
+ zIndex = o.optInt("zIndex", 0),
273
+ )
274
+ }
275
+
276
+ fun lineOptions(o: JSONObject): SMapLineOptions {
277
+ val d = SMapLineOptions()
278
+ val dash = o.optJSONArray("dash")?.let { a -> (0 until a.length()).map { a.getDouble(it).toFloat() } }
279
+ return SMapLineOptions(
280
+ color = o.optStringOrNull("color") ?: d.color,
281
+ width = o.optFloat("width") ?: d.width,
282
+ dash = dash,
283
+ opacity = o.optFloat("opacity") ?: d.opacity,
284
+ minZoom = o.optFloat("minZoom", "minzoom"),
285
+ maxZoom = o.optFloat("maxZoom", "maxzoom"),
286
+ zIndex = o.optInt("zIndex", 0),
287
+ )
288
+ }
289
+
290
+ fun polygonOptions(o: JSONObject): SMapPolygonOptions {
291
+ val d = SMapPolygonOptions()
292
+ val extrude = o.optJSONObject("extrude")?.let { e -> SMapExtrude(height = value(e, "height", 20.0), base = value(e, "base", 0.0)) }
293
+ return SMapPolygonOptions(
294
+ fill = o.optStringOrNull("fill") ?: o.optStringOrNull("color") ?: d.fill,
295
+ opacity = o.optFloat("opacity") ?: d.opacity,
296
+ outline = o.optStringOrNull("outline"),
297
+ outlineWidth = o.optFloat("outlineWidth") ?: d.outlineWidth,
298
+ extrude = extrude,
299
+ minZoom = o.optFloat("minZoom", "minzoom"),
300
+ maxZoom = o.optFloat("maxZoom", "maxzoom"),
301
+ zIndex = o.optInt("zIndex", 0),
302
+ )
303
+ }
304
+
305
+ private fun jsonSafe(value: Map<String, Any?>): Map<String, Any?> = value.mapValues { (_, v) -> toJsonValue(v) }
306
+
307
+ private fun toJsonValue(v: Any?): Any? = when (v) {
308
+ null -> JSONObject.NULL
309
+ is Map<*, *> -> JSONObject(v.entries.associate { it.key.toString() to toJsonValue(it.value) })
310
+ is List<*> -> JSONArray(v.map { toJsonValue(it) })
311
+ else -> v
312
+ }
313
+ }
314
+ }
@@ -0,0 +1,71 @@
1
+ package ca.superroute.smap.reactnative
2
+
3
+ import com.facebook.react.module.annotations.ReactModule
4
+ import com.facebook.react.uimanager.SimpleViewManager
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.ViewManagerDelegate
7
+ import com.facebook.react.uimanager.annotations.ReactProp
8
+ import com.facebook.react.viewmanagers.SMapNativeViewManagerDelegate
9
+ import com.facebook.react.viewmanagers.SMapNativeViewManagerInterface
10
+
11
+ /** The Fabric view manager of `SMapNativeView`; props and commands are routed by the generated delegate. */
12
+ @ReactModule(name = SMapViewManager.NAME)
13
+ class SMapViewManager : SimpleViewManager<SMapReactView>(), SMapNativeViewManagerInterface<SMapReactView> {
14
+ private val delegate: ViewManagerDelegate<SMapReactView> = SMapNativeViewManagerDelegate(this)
15
+
16
+ override fun getDelegate(): ViewManagerDelegate<SMapReactView> = delegate
17
+
18
+ override fun getName(): String = NAME
19
+
20
+ override fun createViewInstance(context: ThemedReactContext): SMapReactView = SMapReactView(context)
21
+
22
+ override fun onAfterUpdateTransaction(view: SMapReactView) {
23
+ super.onAfterUpdateTransaction(view)
24
+ view.commitProps()
25
+ }
26
+
27
+ override fun onDropViewInstance(view: SMapReactView) {
28
+ view.destroy()
29
+ super.onDropViewInstance(view)
30
+ }
31
+
32
+ override fun getExportedCustomDirectEventTypeConstants(): Map<String, Any> = SMapReactView.EVENTS.associate {
33
+ "top" + it.replaceFirstChar(Char::uppercaseChar) to mapOf("registrationName" to "on" + it.replaceFirstChar(Char::uppercaseChar))
34
+ }
35
+
36
+ @ReactProp(name = "config")
37
+ override fun setConfig(view: SMapReactView, value: String?) { view.config = value }
38
+
39
+ @ReactProp(name = "mapStyle")
40
+ override fun setMapStyle(view: SMapReactView, value: String?) { view.setFlavor(value) }
41
+
42
+ @ReactProp(name = "language")
43
+ override fun setLanguage(view: SMapReactView, value: String?) { view.setLanguageTag(value) }
44
+
45
+ @ReactProp(name = "camera")
46
+ override fun setCamera(view: SMapReactView, value: String?) { view.setCamera(value) }
47
+
48
+ @ReactProp(name = "buildings3D")
49
+ override fun setBuildings3D(view: SMapReactView, value: Boolean) { view.setBuildings(value) }
50
+
51
+ override fun addLayer(view: SMapReactView, kind: String, id: String, data: String, options: String) =
52
+ view.addLayer(kind, id, data, options)
53
+
54
+ override fun setData(view: SMapReactView, id: String, data: String) = view.setLayerData(id, data)
55
+
56
+ override fun setVisible(view: SMapReactView, id: String, visible: Boolean) = view.setLayerVisible(id, visible)
57
+
58
+ override fun removeLayer(view: SMapReactView, id: String) = view.removeLayer(id)
59
+
60
+ override fun fitLayer(view: SMapReactView, id: String, padding: Double, maxZoom: Double) =
61
+ view.fitLayer(id, padding, maxZoom)
62
+
63
+ override fun flyTo(view: SMapReactView, lat: Double, lng: Double, zoom: Double) = view.flyTo(lat, lng, zoom)
64
+
65
+ override fun resolveToken(view: SMapReactView, requestId: Int, token: String, error: String) =
66
+ view.resolveToken(requestId, token, error)
67
+
68
+ companion object {
69
+ const val NAME = "SMapNativeView"
70
+ }
71
+ }