@tileflow/react-native 0.0.0-bootstrap.0 → 0.1.0-alpha.0
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/README.md +216 -4
- package/TileflowNativeAdmission.podspec +34 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrap.kt +144 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionBootstrapOkHttpNetwork.kt +89 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionCatalog.kt +137 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionDocumentScope.kt +22 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionEngine.kt +335 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionModuleProvider.kt +68 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionOkHttpNetwork.kt +86 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionProviderLease.kt +56 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionTypes.kt +85 -0
- package/android/src/main/java/dev/tileflow/reactnative/AdmissionUrl.kt +100 -0
- package/android/src/main/java/dev/tileflow/reactnative/HandlerAdmissionScheduler.kt +16 -0
- package/android/src/main/java/dev/tileflow/reactnative/MobileConfiguration.kt +95 -0
- package/android/src/main/java/dev/tileflow/reactnative/MobileConfigurationResources.kt +43 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentOkHttpNetwork.kt +101 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeDocumentRegistry.kt +136 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeSurfaceState.kt +115 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionModule.kt +234 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeAdmissionPackage.kt +16 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeConfigurationModule.kt +43 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeDocumentsModule.kt +80 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeSurfaceModule.kt +277 -0
- package/android/src/main/res/raw/tileflow_configuration_keep.xml +3 -0
- package/dist/contract-DWs-lDPo.d.ts +144 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +6989 -0
- package/dist/internal/camera.d.ts +65 -0
- package/dist/internal/camera.js +325 -0
- package/dist/internal/hosted-binding.d.ts +18 -0
- package/dist/internal/hosted-binding.js +277 -0
- package/dist/internal/initial-view.d.ts +11 -0
- package/dist/internal/initial-view.js +8 -0
- package/dist/internal/interactions.d.ts +76 -0
- package/dist/internal/interactions.js +794 -0
- package/dist/internal/native-admission-bridge.d.ts +29 -0
- package/dist/internal/native-admission-bridge.js +351 -0
- package/dist/internal/native-admission.d.ts +78 -0
- package/dist/internal/native-admission.js +1614 -0
- package/dist/internal/native-appearance.d.ts +21 -0
- package/dist/internal/native-appearance.js +110 -0
- package/dist/internal/native-configuration-bridge.d.ts +9 -0
- package/dist/internal/native-configuration-bridge.js +343 -0
- package/dist/internal/session.d.ts +125 -0
- package/dist/internal/session.js +887 -0
- package/dist/internal/source-state.d.ts +11 -0
- package/dist/internal/source-state.js +26 -0
- package/dist/native-admission-contract-k7LKE1vW.d.ts +89 -0
- package/docs/native-admission.md +108 -0
- package/docs/native-configuration.md +210 -0
- package/docs/native-interactions.md +161 -0
- package/docs/native-location.md +148 -0
- package/ios/TFAdmissionBootstrap.h +17 -0
- package/ios/TFAdmissionBootstrap.mm +168 -0
- package/ios/TFAdmissionCatalog.h +13 -0
- package/ios/TFAdmissionCatalog.mm +193 -0
- package/ios/TFAdmissionDocumentScope.h +7 -0
- package/ios/TFAdmissionDocumentScope.mm +45 -0
- package/ios/TFAdmissionEngine.h +57 -0
- package/ios/TFAdmissionEngine.mm +371 -0
- package/ios/TFAdmissionInstallation.h +27 -0
- package/ios/TFAdmissionInstallation.mm +117 -0
- package/ios/TFAdmissionNetwork.h +16 -0
- package/ios/TFAdmissionNetwork.mm +268 -0
- package/ios/TFAdmissionURL.mm +116 -0
- package/ios/TFDocumentBounds.h +9 -0
- package/ios/TFDocumentBounds.mm +19 -0
- package/ios/TFMobileConfiguration.h +23 -0
- package/ios/TFMobileConfiguration.mm +139 -0
- package/ios/TFNativeAdmissionURLProtocol.mm +69 -0
- package/ios/TFNativeDocumentRegistry.h +28 -0
- package/ios/TFNativeDocumentRegistry.mm +193 -0
- package/ios/TFNativeDocumentURL.h +6 -0
- package/ios/TFNativeSurfaceState.h +28 -0
- package/ios/TFNativeSurfaceState.mm +128 -0
- package/ios/TileflowNativeAdmission.h +9 -0
- package/ios/TileflowNativeAdmission.mm +184 -0
- package/ios/TileflowNativeConfiguration.mm +41 -0
- package/ios/TileflowNativeDocuments.h +12 -0
- package/ios/TileflowNativeDocuments.mm +105 -0
- package/ios/TileflowNativeSurface.mm +403 -0
- package/ios/tileflow_native_admission.rb +45 -0
- package/package.json +51 -7
- package/react-native.config.cjs +13 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
package dev.tileflow.reactnative
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
import android.view.View
|
|
6
|
+
import android.view.ViewGroup
|
|
7
|
+
import android.view.ViewTreeObserver
|
|
8
|
+
import com.facebook.react.bridge.Arguments
|
|
9
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
10
|
+
import com.facebook.react.bridge.Promise
|
|
11
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
12
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
13
|
+
import com.facebook.react.bridge.ReactMethod
|
|
14
|
+
import com.facebook.react.bridge.ReadableMap
|
|
15
|
+
import com.facebook.react.bridge.ReadableType
|
|
16
|
+
import com.facebook.react.common.LifecycleState
|
|
17
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
18
|
+
import com.facebook.react.uimanager.UIManagerHelper
|
|
19
|
+
import com.facebook.react.uimanager.common.UIManagerType
|
|
20
|
+
import java.util.UUID
|
|
21
|
+
import kotlin.math.abs
|
|
22
|
+
import org.maplibre.android.maps.MapLibreMap
|
|
23
|
+
import org.maplibre.android.maps.MapView
|
|
24
|
+
import org.maplibre.android.maps.Style
|
|
25
|
+
import org.maplibre.reactnative.components.camera.MLRNCamera
|
|
26
|
+
import org.maplibre.reactnative.components.mapview.MLRNMapView
|
|
27
|
+
|
|
28
|
+
class TileflowNativeSurfaceModule(context: ReactApplicationContext) : ReactContextBaseJavaModule(context), LifecycleEventListener {
|
|
29
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
30
|
+
private val surfaces = linkedMapOf<String, Surface>()
|
|
31
|
+
private val rootRetirements = linkedMapOf<Int, RootRetirement>()
|
|
32
|
+
private var foreground = context.lifecycleState == LifecycleState.RESUMED
|
|
33
|
+
private var invalidated = false
|
|
34
|
+
private var listeners = 0
|
|
35
|
+
init { context.addLifecycleEventListener(this) }
|
|
36
|
+
override fun getName() = "TileflowNativeSurface"
|
|
37
|
+
@ReactMethod fun addListener(name: String) { handler.post { if (name == "TileflowNativeSurfaceEvent") listeners++ } }
|
|
38
|
+
@ReactMethod fun removeListeners(count: Double) { handler.post { listeners = maxOf(0, listeners - count.toInt()) } }
|
|
39
|
+
private fun invalid(): Nothing = throw IllegalStateException("Native surface operation failed.")
|
|
40
|
+
private fun reject(promise: Promise) { promise.reject("NATIVE_SURFACE_UNAVAILABLE", "Native surface operation failed.") }
|
|
41
|
+
private fun action(promise: Promise, work: () -> Any) {
|
|
42
|
+
handler.post { try { promise.resolve(work()) } catch (_: Exception) { reject(promise) } }
|
|
43
|
+
}
|
|
44
|
+
private fun integer(value: Double): Long {
|
|
45
|
+
if (!value.isFinite() || value < 1 || value > 9007199254740991.0 || value != value.toLong().toDouble()) invalid()
|
|
46
|
+
return value.toLong()
|
|
47
|
+
}
|
|
48
|
+
private fun root(tag: Int): View? {
|
|
49
|
+
return try { UIManagerHelper.getUIManager(reactApplicationContext, UIManagerType.FABRIC)?.resolveView(tag) }
|
|
50
|
+
catch (_: Exception) { null }
|
|
51
|
+
}
|
|
52
|
+
private fun mapIn(root: View): MLRNMapView {
|
|
53
|
+
val queue = ArrayDeque<View>(); queue.add(root)
|
|
54
|
+
var found: MLRNMapView? = null
|
|
55
|
+
var visited = 0
|
|
56
|
+
while (queue.isNotEmpty()) {
|
|
57
|
+
if (++visited > 1024) invalid()
|
|
58
|
+
val view = queue.removeFirst()
|
|
59
|
+
if (view is MLRNMapView) {
|
|
60
|
+
if (found != null) invalid()
|
|
61
|
+
found = view
|
|
62
|
+
} else if (view is ViewGroup) {
|
|
63
|
+
if (view.childCount + queue.size > 1024) invalid()
|
|
64
|
+
for (index in 0 until view.childCount) queue.add(view.getChildAt(index))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return found ?: invalid()
|
|
68
|
+
}
|
|
69
|
+
private fun current(id: String, allowFailed: Boolean = false): Surface {
|
|
70
|
+
val surface = surfaces[id] ?: invalid()
|
|
71
|
+
if (invalidated || surface.retiring || (!allowFailed && !surface.state.active) || root(surface.root.id) !== surface.root || mapIn(surface.root) !== surface.map) invalid()
|
|
72
|
+
return surface
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@ReactMethod fun attachSurface(tag: Double, promise: Promise) = action(promise) {
|
|
76
|
+
if (invalidated || !foreground || listeners < 1 || surfaces.size >= 16 || tag != tag.toInt().toDouble() || tag < 1) invalid()
|
|
77
|
+
val root = root(tag.toInt()) ?: invalid()
|
|
78
|
+
val map = mapIn(root)
|
|
79
|
+
if (!root.isAttachedToWindow || map.mapLibreMap == null || surfaces.values.any { it.map === map }) invalid()
|
|
80
|
+
val surface = Surface(UUID.randomUUID().toString(), root, map)
|
|
81
|
+
surfaces[surface.id] = surface
|
|
82
|
+
try { surface.attach() } catch (_: Exception) { surface.retire(); throw IllegalStateException("Native surface operation failed.") }
|
|
83
|
+
Arguments.makeNativeMap(mapOf("surface" to surface.id))
|
|
84
|
+
}
|
|
85
|
+
@ReactMethod fun expectStyle(id: String, token: String, promise: Promise) = action(promise) {
|
|
86
|
+
val surface = current(id, allowFailed = true)
|
|
87
|
+
surface.state.expect(token); surface.token = token
|
|
88
|
+
surface.deadline?.let { handler.removeCallbacks(it) }
|
|
89
|
+
val deadline = Runnable { if (!surface.retiring && surface.token == token) surface.state.fail() }
|
|
90
|
+
surface.deadline = deadline; handler.postDelayed(deadline, 30000)
|
|
91
|
+
Arguments.makeNativeMap(mapOf("accepted" to true))
|
|
92
|
+
}
|
|
93
|
+
@ReactMethod fun acknowledgeSurface(id: String, sequence: Double, promise: Promise) = action(promise) {
|
|
94
|
+
if (!AdmissionUrl.validToken(id)) invalid()
|
|
95
|
+
surfaces[id]?.state?.acknowledge(integer(sequence))
|
|
96
|
+
Arguments.makeNativeMap(mapOf("acknowledged" to true))
|
|
97
|
+
}
|
|
98
|
+
@ReactMethod fun commitLayout(id: String, token: String, promise: Promise) {
|
|
99
|
+
handler.post {
|
|
100
|
+
try {
|
|
101
|
+
val surface = current(id)
|
|
102
|
+
if (surface.layoutWaiter != null) invalid()
|
|
103
|
+
val observer = surface.root.viewTreeObserver
|
|
104
|
+
var settled = false
|
|
105
|
+
lateinit var listener: ViewTreeObserver.OnPreDrawListener
|
|
106
|
+
val finish: (Boolean) -> Unit = finish@ { success ->
|
|
107
|
+
if (settled) return@finish
|
|
108
|
+
settled = true
|
|
109
|
+
if (observer.isAlive) observer.removeOnPreDrawListener(listener)
|
|
110
|
+
surface.layoutWaiter = null
|
|
111
|
+
try {
|
|
112
|
+
if (!success || current(id) !== surface || surface.token != token) invalid()
|
|
113
|
+
surface.sampleLayout()
|
|
114
|
+
promise.resolve(Arguments.makeNativeMap(mapOf("layout" to surface.state.commit(token))))
|
|
115
|
+
} catch (_: Exception) { reject(promise) }
|
|
116
|
+
}
|
|
117
|
+
listener = ViewTreeObserver.OnPreDrawListener { finish(true); true }
|
|
118
|
+
surface.layoutWaiter = { finish(false) }
|
|
119
|
+
observer.addOnPreDrawListener(listener)
|
|
120
|
+
surface.root.invalidate()
|
|
121
|
+
} catch (_: Exception) { reject(promise) }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
@ReactMethod fun requestFrame(id: String, token: String, promise: Promise) = action(promise) {
|
|
125
|
+
val surface = current(id)
|
|
126
|
+
if (surface.token != token) invalid()
|
|
127
|
+
surface.style() ?: invalid()
|
|
128
|
+
surface.state.request(token)
|
|
129
|
+
surface.sdk.triggerRepaint()
|
|
130
|
+
Arguments.makeNativeMap(mapOf("requested" to true))
|
|
131
|
+
}
|
|
132
|
+
@ReactMethod fun applyCamera(id: String, sequence: Double, input: ReadableMap, promise: Promise) = action(promise) {
|
|
133
|
+
val surface = current(id)
|
|
134
|
+
val target = view(input)
|
|
135
|
+
val number = integer(sequence)
|
|
136
|
+
val invalidation = surface.state.beginCommand(number)
|
|
137
|
+
if (invalidation == 0L) invalid()
|
|
138
|
+
val cameras = (0 until surface.map.featureCount).mapNotNull { surface.map.getFeatureAt(it)?.toView() as? MLRNCamera }
|
|
139
|
+
if (cameras.size != 1) invalid()
|
|
140
|
+
val command = Arguments.makeNativeMap(target + mapOf("duration" to 0))
|
|
141
|
+
cameras.single().handleImperativeStop(command)
|
|
142
|
+
val actual = surface.view()
|
|
143
|
+
val expectedCenter = target["center"] as List<*>
|
|
144
|
+
val actualCenter = actual["center"] as List<*>
|
|
145
|
+
if (!nearAngular(actualCenter[0] as Double, expectedCenter[0] as Double) ||
|
|
146
|
+
abs(actualCenter[1] as Double - expectedCenter[1] as Double) > 0.000001 ||
|
|
147
|
+
abs(actual["zoom"] as Double - target["zoom"] as Double) > 0.000001 ||
|
|
148
|
+
!nearAngular(actual["bearing"] as Double, target["bearing"] as Double) ||
|
|
149
|
+
abs(actual["pitch"] as Double - target["pitch"] as Double) > 0.000001 || current(id) !== surface) invalid()
|
|
150
|
+
Arguments.makeNativeMap(mapOf("command" to number, "invalidation" to invalidation, "view" to target))
|
|
151
|
+
}
|
|
152
|
+
@ReactMethod fun cancelCamera(id: String, sequence: Double, promise: Promise) = action(promise) {
|
|
153
|
+
if (!AdmissionUrl.validToken(id)) invalid()
|
|
154
|
+
surfaces[id]?.state?.cancelCommand(integer(sequence))
|
|
155
|
+
Arguments.makeNativeMap(mapOf("cancelled" to true))
|
|
156
|
+
}
|
|
157
|
+
private fun nearAngular(a: Double, b: Double): Boolean = abs(((a - b + 540) % 360) - 180) <= 0.000001
|
|
158
|
+
private fun view(input: ReadableMap): Map<String, Any> {
|
|
159
|
+
val keys = input.keySetIterator(); var count = 0
|
|
160
|
+
while (keys.hasNextKey()) if (++count > 4 || keys.nextKey() !in setOf("center", "zoom", "bearing", "pitch")) invalid()
|
|
161
|
+
if (count != 4) invalid()
|
|
162
|
+
val center = input.getArray("center") ?: invalid()
|
|
163
|
+
if (center.size() != 2 || center.getType(0) != ReadableType.Number || center.getType(1) != ReadableType.Number) invalid()
|
|
164
|
+
val longitude = center.getDouble(0); val latitude = center.getDouble(1)
|
|
165
|
+
val zoom = input.getDouble("zoom"); val bearing = input.getDouble("bearing"); val pitch = input.getDouble("pitch")
|
|
166
|
+
if (listOf(longitude, latitude, zoom, bearing, pitch).any { !it.isFinite() } || longitude !in -180.0..180.0 ||
|
|
167
|
+
latitude !in -90.0..90.0 || zoom !in 0.0..24.0 || bearing !in -180.0..180.0 || pitch !in 0.0..85.0) invalid()
|
|
168
|
+
return mapOf("center" to listOf(longitude, latitude), "zoom" to zoom, "bearing" to bearing, "pitch" to pitch)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@ReactMethod fun retireSurface(id: String, promise: Promise) {
|
|
172
|
+
handler.post {
|
|
173
|
+
try {
|
|
174
|
+
if (!AdmissionUrl.validToken(id)) invalid()
|
|
175
|
+
val surface = surfaces[id]
|
|
176
|
+
if (surface == null) { promise.resolve(Arguments.makeNativeMap(mapOf("detached" to true))); return@post }
|
|
177
|
+
surface.retire()
|
|
178
|
+
waitForRoot(surface.root, promise)
|
|
179
|
+
} catch (_: Exception) { reject(promise) }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
@ReactMethod fun retireRoot(tag: Double, promise: Promise) {
|
|
183
|
+
handler.post {
|
|
184
|
+
try {
|
|
185
|
+
if (tag < 1 || tag != tag.toInt().toDouble()) invalid()
|
|
186
|
+
val root = root(tag.toInt())
|
|
187
|
+
if (root == null) { promise.resolve(Arguments.makeNativeMap(mapOf("detached" to true))); return@post }
|
|
188
|
+
for (surface in surfaces.values.toList()) if (surface.root === root) surface.retire()
|
|
189
|
+
waitForRoot(root, promise)
|
|
190
|
+
} catch (_: Exception) { reject(promise) }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
private fun waitForRoot(root: View, promise: Promise) {
|
|
194
|
+
if (!root.isAttachedToWindow) { promise.resolve(Arguments.makeNativeMap(mapOf("detached" to true))); return }
|
|
195
|
+
val existing = rootRetirements[root.id]
|
|
196
|
+
if (existing != null) { if (existing.root !== root || existing.waiters.size >= 16) invalid(); existing.waiters.add(promise); return }
|
|
197
|
+
if (rootRetirements.size >= 16) invalid()
|
|
198
|
+
val retirement = RootRetirement(root); retirement.waiters.add(promise); rootRetirements[root.id] = retirement
|
|
199
|
+
root.addOnAttachStateChangeListener(retirement)
|
|
200
|
+
// A deadline can report failed cleanup, never successful detachment.
|
|
201
|
+
handler.postDelayed({ if (rootRetirements[root.id] === retirement) retirement.timeout() }, 30000)
|
|
202
|
+
}
|
|
203
|
+
private inner class RootRetirement(val root: View) : View.OnAttachStateChangeListener {
|
|
204
|
+
val waiters = mutableListOf<Promise>()
|
|
205
|
+
override fun onViewAttachedToWindow(view: View) {}
|
|
206
|
+
override fun onViewDetachedFromWindow(view: View) {
|
|
207
|
+
root.removeOnAttachStateChangeListener(this); rootRetirements.remove(root.id)
|
|
208
|
+
for (promise in waiters.toList()) promise.resolve(Arguments.makeNativeMap(mapOf("detached" to true)))
|
|
209
|
+
waiters.clear()
|
|
210
|
+
}
|
|
211
|
+
fun timeout() { for (promise in waiters.toList()) reject(promise); waiters.clear() }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private inner class Surface(val id: String, val root: View, val map: MLRNMapView) : View.OnAttachStateChangeListener {
|
|
215
|
+
val sdk = map.mapLibreMap ?: invalid()
|
|
216
|
+
val state = NativeSurfaceState(id) { event ->
|
|
217
|
+
if (event["kind"] == "render") deadline?.let { handler.removeCallbacks(it); deadline = null }
|
|
218
|
+
if (!invalidated && listeners > 0) reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java).emit("TileflowNativeSurfaceEvent", Arguments.makeNativeMap(event))
|
|
219
|
+
else invalid()
|
|
220
|
+
}
|
|
221
|
+
var token = ""
|
|
222
|
+
var retiring = false
|
|
223
|
+
var deadline: Runnable? = null
|
|
224
|
+
var layoutWaiter: (() -> Unit)? = null
|
|
225
|
+
private var layoutSnapshot: List<Int>? = null
|
|
226
|
+
fun marker() = "__tileflow_native_style_$token"
|
|
227
|
+
fun style(): Style? = sdk.style?.takeIf { it.isFullyLoaded && it.getLayer(marker()) != null }
|
|
228
|
+
fun view(): Map<String, Any> {
|
|
229
|
+
val position = sdk.cameraPosition; val center = position.target ?: invalid()
|
|
230
|
+
val longitude = ((center.longitude + 540) % 360) - 180
|
|
231
|
+
val bearing = ((position.bearing + 540) % 360) - 180
|
|
232
|
+
return mapOf("center" to listOf(longitude, center.latitude), "zoom" to position.zoom, "bearing" to bearing, "pitch" to position.tilt)
|
|
233
|
+
}
|
|
234
|
+
private fun guarded(action: () -> Unit) { if (!retiring) try { action() } catch (_: Exception) { state.fail() } }
|
|
235
|
+
private val loaded = MapView.OnDidFinishLoadingStyleListener { guarded { style()?.let { state.loaded(token, it) } } }
|
|
236
|
+
private val frameStart = MapView.OnWillStartRenderingFrameListener { guarded { sampleLayout(); state.frameStart(style()) } }
|
|
237
|
+
private val frameEnd = MapView.OnDidFinishRenderingFrameListener { fully, _, _ -> guarded { state.frameEnd(style(), fully) } }
|
|
238
|
+
private val cameraStart = MapLibreMap.OnCameraMoveStartedListener { reason -> guarded {
|
|
239
|
+
if (reason == MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE) state.gestureStart(view())
|
|
240
|
+
else state.gestureEnd(view())
|
|
241
|
+
} }
|
|
242
|
+
private val cameraMove = MapLibreMap.OnCameraMoveListener { guarded { state.gestureChange(view()) } }
|
|
243
|
+
private val cameraEnd = MapLibreMap.OnCameraIdleListener { guarded { state.gestureEnd(view()) } }
|
|
244
|
+
private val layout = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> guarded { sampleLayout() } }
|
|
245
|
+
fun sampleLayout() {
|
|
246
|
+
val snapshot = listOf(root.width, root.height, map.width, map.height, root.left, root.top, if (root.isAttachedToWindow && map.isAttachedToWindow && foreground) 1 else 0)
|
|
247
|
+
if (snapshot == layoutSnapshot) return
|
|
248
|
+
layoutSnapshot = snapshot
|
|
249
|
+
state.layout(snapshot.last() == 1 && snapshot.take(4).all { it > 0 })
|
|
250
|
+
}
|
|
251
|
+
fun attach() {
|
|
252
|
+
root.addOnAttachStateChangeListener(this); root.addOnLayoutChangeListener(layout); map.addOnLayoutChangeListener(layout)
|
|
253
|
+
map.addOnDidFinishLoadingStyleListener(loaded); map.addOnWillStartRenderingFrameListener(frameStart)
|
|
254
|
+
map.addOnDidFinishRenderingFrameListener(frameEnd)
|
|
255
|
+
sdk.addOnCameraMoveStartedListener(cameraStart); sdk.addOnCameraMoveListener(cameraMove); sdk.addOnCameraIdleListener(cameraEnd)
|
|
256
|
+
sampleLayout()
|
|
257
|
+
}
|
|
258
|
+
fun retire() {
|
|
259
|
+
if (retiring) return
|
|
260
|
+
retiring = true; state.close(); deadline?.let { handler.removeCallbacks(it) }; deadline = null
|
|
261
|
+
layoutWaiter?.invoke(); layoutWaiter = null
|
|
262
|
+
root.removeOnLayoutChangeListener(layout); map.removeOnLayoutChangeListener(layout)
|
|
263
|
+
map.removeOnDidFinishLoadingStyleListener(loaded); map.removeOnWillStartRenderingFrameListener(frameStart)
|
|
264
|
+
map.removeOnDidFinishRenderingFrameListener(frameEnd)
|
|
265
|
+
sdk.removeOnCameraMoveStartedListener(cameraStart); sdk.removeOnCameraMoveListener(cameraMove); sdk.removeOnCameraIdleListener(cameraEnd)
|
|
266
|
+
if (!root.isAttachedToWindow) onViewDetachedFromWindow(root)
|
|
267
|
+
}
|
|
268
|
+
override fun onViewAttachedToWindow(view: View) { guarded { sampleLayout() } }
|
|
269
|
+
override fun onViewDetachedFromWindow(view: View) {
|
|
270
|
+
retire(); root.removeOnAttachStateChangeListener(this); surfaces.remove(id)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
override fun onHostPause() { handler.post { foreground = false; for (surface in surfaces.values) surface.sampleLayout() } }
|
|
274
|
+
override fun onHostResume() { handler.post { foreground = true; for (surface in surfaces.values) surface.sampleLayout() } }
|
|
275
|
+
override fun onHostDestroy() { handler.post { invalidated = true; for (surface in surfaces.values.toList()) surface.retire() } }
|
|
276
|
+
override fun invalidate() { reactApplicationContext.removeLifecycleEventListener(this); onHostDestroy(); super.invalidate() }
|
|
277
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { MapProps as MapProps$1 } from '@maplibre/maplibre-react-native';
|
|
2
|
+
import { ReactElement, ReactNode, Ref } from 'react';
|
|
3
|
+
import { ViewProps } from 'react-native';
|
|
4
|
+
import { TileflowNativeInitialView, TileflowNativeInitialViewOptions, TileflowNativeSource, TileflowNativeSourceOptions, TileflowNativeSourceState, TileflowNativeSourceError } from '@tileflow/core/native';
|
|
5
|
+
import { TileflowAnnotation, TileflowInteractionBinding, TileflowInteractionEvent, TileflowResolvedAnnotationTarget, TileflowResolvedPoiFeatureTarget, TileflowInteractionDiagnostic, TileflowAnnotationViewContext } from '@tileflow/interactions';
|
|
6
|
+
|
|
7
|
+
/** Marker composition only. Selection presentation and native handles are not part of this context. */
|
|
8
|
+
type MapMarkerRenderContext<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = Readonly<Pick<TileflowAnnotationViewContext<TAnnotation>, 'annotation' | 'target' | 'content' | 'viewName'>>;
|
|
9
|
+
/** Return marker content; Tileflow supplies the accessible activation wrapper. */
|
|
10
|
+
type MapMarkerRenderer<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = (context: MapMarkerRenderContext<TAnnotation>) => ReactElement;
|
|
11
|
+
/** Mobile reports activation; the application owns any persistent selection. */
|
|
12
|
+
type MapInteractionEvent<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = Readonly<Omit<TileflowInteractionEvent<TAnnotation>, 'target' | 'type'> & {
|
|
13
|
+
target: TileflowResolvedAnnotationTarget<TAnnotation> | TileflowResolvedPoiFeatureTarget;
|
|
14
|
+
type: 'target:activate';
|
|
15
|
+
}>;
|
|
16
|
+
type MapInteractionProps<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = Readonly<{
|
|
17
|
+
annotations?: readonly TAnnotation[];
|
|
18
|
+
interactions?: readonly TileflowInteractionBinding[];
|
|
19
|
+
onInteractionEvent?: (event: MapInteractionEvent<TAnnotation>) => void;
|
|
20
|
+
onInteractionDiagnostic?: (diagnostic: TileflowInteractionDiagnostic) => void;
|
|
21
|
+
renderMarker?: MapMarkerRenderer<TAnnotation>;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
type MapSource = TileflowNativeSource;
|
|
25
|
+
type MapThemeSelection = NonNullable<TileflowNativeSourceOptions['theme']>;
|
|
26
|
+
type MapColorScheme = NonNullable<TileflowNativeSourceOptions['colorScheme']>;
|
|
27
|
+
type MapView = TileflowNativeInitialView;
|
|
28
|
+
/** Portable composition inputs, independent from source acquisition. */
|
|
29
|
+
type MapInitialViewInputs = TileflowNativeInitialViewOptions;
|
|
30
|
+
type MapSourceProps = Readonly<{
|
|
31
|
+
source: MapSource;
|
|
32
|
+
theme?: MapThemeSelection;
|
|
33
|
+
}>;
|
|
34
|
+
type AllowedMapOptions = 'dragPan' | 'touchZoom' | 'doubleTapZoom' | 'doubleTapHoldZoom' | 'touchRotate' | 'touchPitch' | 'compass' | 'compassHiddenFacingNorth' | 'scaleBar';
|
|
35
|
+
type OwnedMapOptions = Exclude<keyof MapProps$1, AllowedMapOptions> | 'container' | 'styleURL' | 'styleUrl' | 'transformRequest' | 'requestTransform' | 'requestHeaders';
|
|
36
|
+
/** A positive allowlist. Style, interception, view and lifecycle remain adapter-owned. */
|
|
37
|
+
type MapOptions = Readonly<Pick<MapProps$1, AllowedMapOptions>> & {
|
|
38
|
+
readonly [Key in OwnedMapOptions]?: never;
|
|
39
|
+
};
|
|
40
|
+
type MapPresentationProps = Readonly<{
|
|
41
|
+
children?: ReactNode;
|
|
42
|
+
style?: ViewProps['style'];
|
|
43
|
+
testID?: ViewProps['testID'];
|
|
44
|
+
mapOptions?: MapOptions;
|
|
45
|
+
}>;
|
|
46
|
+
type TileflowReady = Extract<TileflowNativeSourceState, {
|
|
47
|
+
status: 'ready';
|
|
48
|
+
}>;
|
|
49
|
+
type MapTheme = Readonly<Pick<TileflowReady['theme'], 'name' | 'colorScheme'>>;
|
|
50
|
+
type MapSourceError = Readonly<Pick<TileflowNativeSourceError, 'code' | 'field' | 'kind'>>;
|
|
51
|
+
/** Safe Tileflow identity only: no URL, style body, credentials or native object. */
|
|
52
|
+
type MapSelection = Readonly<{
|
|
53
|
+
map: TileflowReady['map']['name'];
|
|
54
|
+
theme: MapTheme;
|
|
55
|
+
}>;
|
|
56
|
+
/** Source readiness is not renderer/frame readiness. */
|
|
57
|
+
type MapSourceState = Readonly<{
|
|
58
|
+
status: 'loading';
|
|
59
|
+
generation: number;
|
|
60
|
+
}> | (Readonly<{
|
|
61
|
+
status: 'ready';
|
|
62
|
+
generation: number;
|
|
63
|
+
}> & MapSelection) | Readonly<{
|
|
64
|
+
status: 'error';
|
|
65
|
+
generation: number;
|
|
66
|
+
error: MapSourceError;
|
|
67
|
+
}>;
|
|
68
|
+
/** A synchronous read of the last source snapshot. Does not trigger loading or native commands. */
|
|
69
|
+
type MapRef = Readonly<{
|
|
70
|
+
getSourceState(): MapSourceState | undefined;
|
|
71
|
+
}>;
|
|
72
|
+
/** The current style was accepted by the renderer; this alone does not establish a rendered frame. */
|
|
73
|
+
type MapLoadEvent = Readonly<{
|
|
74
|
+
type: 'load';
|
|
75
|
+
generation: number;
|
|
76
|
+
selection: MapSelection;
|
|
77
|
+
}>;
|
|
78
|
+
/** Renderer failures expose no native event, message or remote cause. */
|
|
79
|
+
type MapErrorEvent = Readonly<{
|
|
80
|
+
type: 'source-error';
|
|
81
|
+
generation: number;
|
|
82
|
+
error: MapSourceError;
|
|
83
|
+
}> | Readonly<{
|
|
84
|
+
type: 'renderer-error';
|
|
85
|
+
generation: number;
|
|
86
|
+
}>;
|
|
87
|
+
/** The renderer owner must confirm readiness; a ready manifest alone is insufficient. */
|
|
88
|
+
type MapReadinessChangeEvent = Readonly<{
|
|
89
|
+
type: 'readiness-change';
|
|
90
|
+
generation: number;
|
|
91
|
+
}> & (Readonly<{
|
|
92
|
+
status: 'loading';
|
|
93
|
+
}> | Readonly<{
|
|
94
|
+
status: 'ready';
|
|
95
|
+
}> | Readonly<{
|
|
96
|
+
status: 'error';
|
|
97
|
+
}>);
|
|
98
|
+
/** Theme transitions retain concrete names; source/renderer error details use onError. */
|
|
99
|
+
type MapThemeChangeEvent = Readonly<{
|
|
100
|
+
type: 'theme-change';
|
|
101
|
+
generation: number;
|
|
102
|
+
map: TileflowReady['map']['name'];
|
|
103
|
+
}> & (Readonly<{
|
|
104
|
+
phase: 'preloading' | 'applying';
|
|
105
|
+
targetTheme: MapTheme;
|
|
106
|
+
currentTheme?: MapTheme;
|
|
107
|
+
}> | Readonly<{
|
|
108
|
+
phase: 'ready';
|
|
109
|
+
currentTheme: MapTheme;
|
|
110
|
+
}> | Readonly<{
|
|
111
|
+
phase: 'error';
|
|
112
|
+
targetTheme?: MapTheme;
|
|
113
|
+
currentTheme?: MapTheme;
|
|
114
|
+
}>);
|
|
115
|
+
type MapEventHandlers = Readonly<{
|
|
116
|
+
onLoad?: (event: MapLoadEvent) => void;
|
|
117
|
+
onError?: (event: MapErrorEvent) => void;
|
|
118
|
+
onReadinessChange?: (event: MapReadinessChangeEvent) => void;
|
|
119
|
+
onThemeChange?: (event: MapThemeChangeEvent) => void;
|
|
120
|
+
}>;
|
|
121
|
+
/** Common source/presentation/lifecycle pieces; MapProps adds camera ownership. */
|
|
122
|
+
type MapBaseProps = MapSourceProps & MapPresentationProps & MapEventHandlers & Readonly<{
|
|
123
|
+
ref?: Ref<MapRef>;
|
|
124
|
+
}>;
|
|
125
|
+
/** Canonical user-driven change only. Commands, renderer events and source generations stay private. */
|
|
126
|
+
type MapViewChangeEvent = Readonly<{
|
|
127
|
+
type: 'view-change';
|
|
128
|
+
view: MapView;
|
|
129
|
+
}>;
|
|
130
|
+
/** The ownership mode is fixed for one mounted native instance. */
|
|
131
|
+
type MapCameraProps = Readonly<{
|
|
132
|
+
view: MapView;
|
|
133
|
+
initialView?: never;
|
|
134
|
+
onViewChange: (event: MapViewChangeEvent) => void;
|
|
135
|
+
}> | Readonly<{
|
|
136
|
+
view?: never;
|
|
137
|
+
initialView?: MapInitialViewInputs['view'];
|
|
138
|
+
/** Observation alone does not make the camera controlled. */
|
|
139
|
+
onViewChange?: (event: MapViewChangeEvent) => void;
|
|
140
|
+
}>;
|
|
141
|
+
/** Complete Tileflow source, presentation, mount-stable camera and portable interaction inputs. */
|
|
142
|
+
type MapProps<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = MapBaseProps & MapCameraProps & MapInteractionProps<TAnnotation>;
|
|
143
|
+
|
|
144
|
+
export type { MapView as M, MapViewChangeEvent as a, MapInitialViewInputs as b, MapCameraProps as c, MapProps as d, MapBaseProps as e, MapColorScheme as f, MapErrorEvent as g, MapEventHandlers as h, MapInteractionEvent as i, MapInteractionProps as j, MapLoadEvent as k, MapMarkerRenderContext as l, MapMarkerRenderer as m, MapOptions as n, MapPresentationProps as o, MapReadinessChangeEvent as p, MapRef as q, MapSelection as r, MapSource as s, MapSourceError as t, MapSourceProps as u, MapSourceState as v, MapTheme as w, MapThemeChangeEvent as x, MapThemeSelection as y };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ReactElement } from 'react';
|
|
2
|
+
import { TileflowAnnotation } from '@tileflow/interactions';
|
|
3
|
+
export { TileflowAnnotation, TileflowInteractionBinding, TileflowInteractionCoordinate, TileflowInteractionDiagnostic, TileflowPoiCategory, TileflowResolvedAnnotationTarget, TileflowResolvedPoiFeatureTarget } from '@tileflow/interactions';
|
|
4
|
+
import { d as MapProps } from './contract-DWs-lDPo.js';
|
|
5
|
+
export { e as MapBaseProps, c as MapCameraProps, f as MapColorScheme, g as MapErrorEvent, h as MapEventHandlers, b as MapInitialViewInputs, i as MapInteractionEvent, j as MapInteractionProps, k as MapLoadEvent, l as MapMarkerRenderContext, m as MapMarkerRenderer, n as MapOptions, o as MapPresentationProps, p as MapReadinessChangeEvent, q as MapRef, r as MapSelection, s as MapSource, t as MapSourceError, u as MapSourceProps, v as MapSourceState, w as MapTheme, x as MapThemeChangeEvent, y as MapThemeSelection, M as MapView, a as MapViewChangeEvent } from './contract-DWs-lDPo.js';
|
|
6
|
+
import '@maplibre/maplibre-react-native';
|
|
7
|
+
import 'react-native';
|
|
8
|
+
import '@tileflow/core/native';
|
|
9
|
+
|
|
10
|
+
/** A Tileflow-only native Map. Its imperative ref exposes safe source state only. */
|
|
11
|
+
declare function Map<TAnnotation extends TileflowAnnotation = TileflowAnnotation>(props: MapProps<TAnnotation>): ReactElement;
|
|
12
|
+
|
|
13
|
+
export { Map, MapProps };
|