@tileflow/react-native 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +15 -0
- package/android/src/main/java/dev/tileflow/reactnative/NativeTheme.kt +75 -0
- package/android/src/main/java/dev/tileflow/reactnative/TileflowNativeSurfaceModule.kt +211 -3
- package/dist/{contract-DWs-lDPo.d.ts → contract-Bwl-gB-I.d.ts} +27 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +648 -182
- package/dist/internal/camera.d.ts +1 -1
- package/dist/internal/initial-view.d.ts +1 -1
- package/dist/internal/native-appearance.d.ts +1 -1
- package/dist/internal/source-state.d.ts +1 -1
- package/ios/TFNativeTheme.h +28 -0
- package/ios/TFNativeTheme.mm +198 -0
- package/ios/TileflowNativeSurface.mm +253 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -85,6 +85,21 @@ and protected delivery still require HTTPS.
|
|
|
85
85
|
|
|
86
86
|
`onThemeChange` reports only the safe public selection shape. It does not expose style URLs, style JSON, configuration, session authority or native error messages.
|
|
87
87
|
|
|
88
|
+
## Theme transitions and blends
|
|
89
|
+
|
|
90
|
+
`themeTransition={{duration: 450}}` cross-fades theme changes: a snapshot of the drawn map covers it, follows the camera and fades out once the new theme is drawn. `themeBlend={{themes: ['night', 'dusk', 'day'], position}}` shows a continuous blend of the map's themes instead of `theme`; position 0 is the first theme and 1.5 is halfway from the second to the third:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
<Map
|
|
94
|
+
source={source}
|
|
95
|
+
themeTransition={{duration: 450}}
|
|
96
|
+
themeBlend={{themes: ['night', 'dusk', 'day'], position}}
|
|
97
|
+
style={{flex: 1}}
|
|
98
|
+
/>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The first blend of a set of themes prepares every theme's style and replaces the native style once; moving `position` later changes the map without loading. Removing `themeBlend` returns to `theme`. Both keep the same native Map, context and session, and `ready` events wait for the fade. The device's reduce-motion setting changes at once. See [theme transitions and blends](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/theme-transitions-and-blends.md#react-native) for what follows the position on native maps.
|
|
102
|
+
|
|
88
103
|
## Camera
|
|
89
104
|
|
|
90
105
|
The existing mount-stable camera contract is preserved. Use either an initial camera seed or complete controlled camera ownership for the lifetime of one mounted Map:
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package dev.tileflow.reactnative
|
|
2
|
+
|
|
3
|
+
import android.animation.ValueAnimator
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.graphics.Bitmap
|
|
6
|
+
import android.graphics.Canvas
|
|
7
|
+
import android.graphics.Matrix
|
|
8
|
+
import android.graphics.Paint
|
|
9
|
+
import android.graphics.PorterDuff
|
|
10
|
+
import android.graphics.PorterDuffXfermode
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.provider.Settings
|
|
13
|
+
import com.facebook.react.bridge.Dynamic
|
|
14
|
+
import com.facebook.react.bridge.ReadableType
|
|
15
|
+
import kotlin.math.roundToInt
|
|
16
|
+
|
|
17
|
+
/** Pure helpers for theme transitions and blends on MapLibre Android. */
|
|
18
|
+
internal object NativeTheme {
|
|
19
|
+
private const val maximumDepth = 32
|
|
20
|
+
|
|
21
|
+
/** A style-specification value from JavaScript as the plain Java value MapLibre converts. */
|
|
22
|
+
fun value(dynamic: Dynamic, depth: Int = 0): Any? {
|
|
23
|
+
if (depth > maximumDepth) throw IllegalStateException("Native surface operation failed.")
|
|
24
|
+
return when (dynamic.type) {
|
|
25
|
+
ReadableType.Null -> null
|
|
26
|
+
ReadableType.Boolean -> dynamic.asBoolean()
|
|
27
|
+
ReadableType.Number -> dynamic.asDouble().also { if (!it.isFinite()) throw IllegalStateException("Native surface operation failed.") }
|
|
28
|
+
ReadableType.String -> dynamic.asString()
|
|
29
|
+
ReadableType.Array -> {
|
|
30
|
+
val array = dynamic.asArray() ?: throw IllegalStateException("Native surface operation failed.")
|
|
31
|
+
Array(array.size()) { index -> value(array.getDynamic(index), depth + 1) }
|
|
32
|
+
}
|
|
33
|
+
ReadableType.Map -> {
|
|
34
|
+
val map = dynamic.asMap() ?: throw IllegalStateException("Native surface operation failed.")
|
|
35
|
+
val result = HashMap<String, Any?>()
|
|
36
|
+
val keys = map.keySetIterator()
|
|
37
|
+
while (keys.hasNextKey()) {
|
|
38
|
+
val key = keys.nextKey()
|
|
39
|
+
result[key] = value(map.getDynamic(key), depth + 1)
|
|
40
|
+
}
|
|
41
|
+
result
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether the device shows animations; the "remove animations" setting turns them off. */
|
|
47
|
+
fun motionEnabled(context: Context): Boolean {
|
|
48
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !ValueAnimator.areAnimatorsEnabled()) return false
|
|
49
|
+
return Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f) != 0f
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Pattern artwork mixed in premultiplied pixels: `from` at 1 - t plus `to` at t. */
|
|
53
|
+
fun mix(from: Bitmap, to: Bitmap, t: Double): Bitmap {
|
|
54
|
+
if (t <= 0) return from
|
|
55
|
+
if (t >= 1) return to
|
|
56
|
+
if (from.width != to.width || from.height != to.height) return if (t < 0.5) from else to
|
|
57
|
+
val result = Bitmap.createBitmap(from.width, from.height, Bitmap.Config.ARGB_8888)
|
|
58
|
+
result.density = from.density
|
|
59
|
+
val canvas = Canvas(result)
|
|
60
|
+
val paint = Paint()
|
|
61
|
+
paint.alpha = ((1 - t) * 255).roundToInt()
|
|
62
|
+
canvas.drawBitmap(from, 0f, 0f, paint)
|
|
63
|
+
paint.alpha = (t * 255).roundToInt()
|
|
64
|
+
paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.ADD)
|
|
65
|
+
canvas.drawBitmap(to, 0f, 0f, paint)
|
|
66
|
+
return result
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The projective transform taking four points to four points, or null when degenerate. */
|
|
70
|
+
fun homography(from: FloatArray, to: FloatArray): Matrix? {
|
|
71
|
+
if (from.size != 8 || to.size != 8 || to.any { !it.isFinite() }) return null
|
|
72
|
+
val matrix = Matrix()
|
|
73
|
+
return if (matrix.setPolyToPoly(from, 0, to, 0, 4)) matrix else null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
package dev.tileflow.reactnative
|
|
2
2
|
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.PointF
|
|
3
5
|
import android.os.Handler
|
|
4
6
|
import android.os.Looper
|
|
5
7
|
import android.view.View
|
|
6
8
|
import android.view.ViewGroup
|
|
9
|
+
import android.view.PixelCopy
|
|
10
|
+
import android.view.SurfaceView
|
|
11
|
+
import android.view.TextureView
|
|
7
12
|
import android.view.ViewTreeObserver
|
|
13
|
+
import android.view.animation.AccelerateDecelerateInterpolator
|
|
14
|
+
import android.widget.ImageView
|
|
8
15
|
import com.facebook.react.bridge.Arguments
|
|
9
16
|
import com.facebook.react.bridge.LifecycleEventListener
|
|
10
17
|
import com.facebook.react.bridge.Promise
|
|
@@ -19,9 +26,14 @@ import com.facebook.react.uimanager.UIManagerHelper
|
|
|
19
26
|
import com.facebook.react.uimanager.common.UIManagerType
|
|
20
27
|
import java.util.UUID
|
|
21
28
|
import kotlin.math.abs
|
|
29
|
+
import org.maplibre.android.geometry.LatLng
|
|
22
30
|
import org.maplibre.android.maps.MapLibreMap
|
|
23
31
|
import org.maplibre.android.maps.MapView
|
|
24
32
|
import org.maplibre.android.maps.Style
|
|
33
|
+
import org.maplibre.android.style.layers.Layer
|
|
34
|
+
import org.maplibre.android.style.layers.LayoutPropertyValue
|
|
35
|
+
import org.maplibre.android.style.layers.PaintPropertyValue
|
|
36
|
+
import org.maplibre.android.style.light.Position
|
|
25
37
|
import org.maplibre.reactnative.components.camera.MLRNCamera
|
|
26
38
|
import org.maplibre.reactnative.components.mapview.MLRNMapView
|
|
27
39
|
|
|
@@ -154,6 +166,35 @@ class TileflowNativeSurfaceModule(context: ReactApplicationContext) : ReactConte
|
|
|
154
166
|
surfaces[id]?.state?.cancelCommand(integer(sequence))
|
|
155
167
|
Arguments.makeNativeMap(mapOf("cancelled" to true))
|
|
156
168
|
}
|
|
169
|
+
private fun duration(value: Double): Long {
|
|
170
|
+
if (!value.isFinite() || value < 0 || value > 5000 || value != value.toLong().toDouble()) invalid()
|
|
171
|
+
return value.toLong()
|
|
172
|
+
}
|
|
173
|
+
@ReactMethod fun coverSurface(id: String, milliseconds: Double, promise: Promise) {
|
|
174
|
+
handler.post {
|
|
175
|
+
try {
|
|
176
|
+
val surface = current(id)
|
|
177
|
+
val covered = { value: Boolean -> promise.resolve(Arguments.makeNativeMap(mapOf("covered" to value))) }
|
|
178
|
+
if (duration(milliseconds) == 0L) covered(false) else surface.cover(covered)
|
|
179
|
+
} catch (_: Exception) { reject(promise) }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
@ReactMethod fun revealSurface(id: String, milliseconds: Double, promise: Promise) {
|
|
183
|
+
handler.post {
|
|
184
|
+
try { current(id, allowFailed = true).reveal(duration(milliseconds), promise) }
|
|
185
|
+
catch (_: Exception) { reject(promise) }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
@ReactMethod fun discardSurfaceCover(id: String, promise: Promise) = action(promise) {
|
|
189
|
+
if (!AdmissionUrl.validToken(id)) invalid()
|
|
190
|
+
surfaces[id]?.discardCovers()
|
|
191
|
+
Arguments.makeNativeMap(mapOf("discarded" to true))
|
|
192
|
+
}
|
|
193
|
+
@ReactMethod fun applyThemeValues(id: String, token: String, values: ReadableMap, promise: Promise) = action(promise) {
|
|
194
|
+
val surface = current(id)
|
|
195
|
+
if (surface.token != token) invalid()
|
|
196
|
+
Arguments.makeNativeMap(mapOf("applied" to surface.applyThemeValues(surface.style() ?: invalid(), values)))
|
|
197
|
+
}
|
|
157
198
|
private fun nearAngular(a: Double, b: Double): Boolean = abs(((a - b + 540) % 360) - 180) <= 0.000001
|
|
158
199
|
private fun view(input: ReadableMap): Map<String, Any> {
|
|
159
200
|
val keys = input.keySetIterator(); var count = 0
|
|
@@ -222,6 +263,13 @@ class TileflowNativeSurfaceModule(context: ReactApplicationContext) : ReactConte
|
|
|
222
263
|
var retiring = false
|
|
223
264
|
var deadline: Runnable? = null
|
|
224
265
|
var layoutWaiter: (() -> Unit)? = null
|
|
266
|
+
private val covers = mutableListOf<Cover>()
|
|
267
|
+
private val reveals = mutableListOf<Reveal>()
|
|
268
|
+
private val settling = mutableListOf<Reveal>()
|
|
269
|
+
private var revealDeadline: Runnable? = null
|
|
270
|
+
private var themedToken: String? = null
|
|
271
|
+
private val themedLayers = HashMap<String, Layer>()
|
|
272
|
+
private val themedImages = HashMap<String, Bitmap>()
|
|
225
273
|
private var layoutSnapshot: List<Int>? = null
|
|
226
274
|
fun marker() = "__tileflow_native_style_$token"
|
|
227
275
|
fun style(): Style? = sdk.style?.takeIf { it.isFullyLoaded && it.getLayer(marker()) != null }
|
|
@@ -233,13 +281,16 @@ class TileflowNativeSurfaceModule(context: ReactApplicationContext) : ReactConte
|
|
|
233
281
|
}
|
|
234
282
|
private fun guarded(action: () -> Unit) { if (!retiring) try { action() } catch (_: Exception) { state.fail() } }
|
|
235
283
|
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 {
|
|
284
|
+
private val frameStart = MapView.OnWillStartRenderingFrameListener { guarded { sampleLayout(); state.frameStart(style()); followCovers() } }
|
|
285
|
+
private val frameEnd = MapView.OnDidFinishRenderingFrameListener { fully, _, _ -> guarded {
|
|
286
|
+
state.frameEnd(style(), fully)
|
|
287
|
+
if (fully && reveals.isNotEmpty()) startReveals()
|
|
288
|
+
} }
|
|
238
289
|
private val cameraStart = MapLibreMap.OnCameraMoveStartedListener { reason -> guarded {
|
|
239
290
|
if (reason == MapLibreMap.OnCameraMoveStartedListener.REASON_API_GESTURE) state.gestureStart(view())
|
|
240
291
|
else state.gestureEnd(view())
|
|
241
292
|
} }
|
|
242
|
-
private val cameraMove = MapLibreMap.OnCameraMoveListener { guarded { state.gestureChange(view()) } }
|
|
293
|
+
private val cameraMove = MapLibreMap.OnCameraMoveListener { guarded { state.gestureChange(view()); followCovers() } }
|
|
243
294
|
private val cameraEnd = MapLibreMap.OnCameraIdleListener { guarded { state.gestureEnd(view()) } }
|
|
244
295
|
private val layout = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> guarded { sampleLayout() } }
|
|
245
296
|
fun sampleLayout() {
|
|
@@ -257,6 +308,7 @@ class TileflowNativeSurfaceModule(context: ReactApplicationContext) : ReactConte
|
|
|
257
308
|
}
|
|
258
309
|
fun retire() {
|
|
259
310
|
if (retiring) return
|
|
311
|
+
discardCovers()
|
|
260
312
|
retiring = true; state.close(); deadline?.let { handler.removeCallbacks(it) }; deadline = null
|
|
261
313
|
layoutWaiter?.invoke(); layoutWaiter = null
|
|
262
314
|
root.removeOnLayoutChangeListener(layout); map.removeOnLayoutChangeListener(layout)
|
|
@@ -265,6 +317,162 @@ class TileflowNativeSurfaceModule(context: ReactApplicationContext) : ReactConte
|
|
|
265
317
|
sdk.removeOnCameraMoveStartedListener(cameraStart); sdk.removeOnCameraMoveListener(cameraMove); sdk.removeOnCameraIdleListener(cameraEnd)
|
|
266
318
|
if (!root.isAttachedToWindow) onViewDetachedFromWindow(root)
|
|
267
319
|
}
|
|
320
|
+
/** A snapshot of one frame over the rendering view that follows four of its ground points. */
|
|
321
|
+
inner class Cover(val view: ImageView, val corners: FloatArray, val anchors: List<LatLng>) { var fading = false }
|
|
322
|
+
inner class Reveal(val duration: Long, val covers: List<Cover>, var promise: Promise?)
|
|
323
|
+
|
|
324
|
+
fun cover(covered: (Boolean) -> Unit) {
|
|
325
|
+
val render = map.renderView
|
|
326
|
+
if (!NativeTheme.motionEnabled(reactApplicationContext) || !foreground || style() == null || render == null ||
|
|
327
|
+
render.width < 1 || render.height < 1 || covers.size >= 4) { covered(false); return }
|
|
328
|
+
if (covers.any { !it.fading }) { covered(true); return }
|
|
329
|
+
// Only the drawn map is copied; logo, attribution and markers stay live above the cover.
|
|
330
|
+
when (render) {
|
|
331
|
+
is TextureView -> covered(render.bitmap?.let { addCover(it) } ?: false)
|
|
332
|
+
is SurfaceView -> {
|
|
333
|
+
val bitmap = Bitmap.createBitmap(render.width, render.height, Bitmap.Config.ARGB_8888)
|
|
334
|
+
PixelCopy.request(render, bitmap, { result ->
|
|
335
|
+
covered(try { result == PixelCopy.SUCCESS && addCover(bitmap) } catch (_: Exception) { false })
|
|
336
|
+
}, handler)
|
|
337
|
+
}
|
|
338
|
+
else -> covered(false)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
private fun addCover(bitmap: Bitmap): Boolean {
|
|
342
|
+
val render = map.renderView
|
|
343
|
+
if (retiring || style() == null || render == null || !NativeTheme.motionEnabled(reactApplicationContext)) return false
|
|
344
|
+
val width = bitmap.width.toFloat(); val height = bitmap.height.toFloat()
|
|
345
|
+
// Ground points below the horizon at any supported pitch, in snapshot pixels.
|
|
346
|
+
val corners = floatArrayOf(width * 0.1f, height * 0.45f, width * 0.9f, height * 0.45f, width * 0.9f, height * 0.9f, width * 0.1f, height * 0.9f)
|
|
347
|
+
val projection = sdk.projection
|
|
348
|
+
val anchors = (0 until 4).map { projection.fromScreenLocation(PointF(corners[2 * it], corners[2 * it + 1])) }
|
|
349
|
+
if (anchors.any { !it.latitude.isFinite() || !it.longitude.isFinite() }) return false
|
|
350
|
+
val view = ImageView(map.context)
|
|
351
|
+
view.scaleType = ImageView.ScaleType.MATRIX
|
|
352
|
+
view.setImageBitmap(bitmap)
|
|
353
|
+
view.isClickable = false; view.isFocusable = false
|
|
354
|
+
view.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
|
355
|
+
// Newer covers go below older ones, which keep fading on top.
|
|
356
|
+
map.addView(view, map.indexOfChild(render) + 1, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
|
|
357
|
+
// React Native lays out only the views it manages, so the cover is sized here.
|
|
358
|
+
view.measure(View.MeasureSpec.makeMeasureSpec(map.width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(map.height, View.MeasureSpec.EXACTLY))
|
|
359
|
+
view.layout(0, 0, map.width, map.height)
|
|
360
|
+
covers.add(Cover(view, corners, anchors))
|
|
361
|
+
followCovers()
|
|
362
|
+
return true
|
|
363
|
+
}
|
|
364
|
+
private fun followCovers() {
|
|
365
|
+
if (covers.isEmpty()) return
|
|
366
|
+
val projection = sdk.projection
|
|
367
|
+
for (cover in covers) {
|
|
368
|
+
val target = FloatArray(8)
|
|
369
|
+
cover.anchors.forEachIndexed { index, anchor ->
|
|
370
|
+
val point = projection.toScreenLocation(anchor)
|
|
371
|
+
target[2 * index] = point.x; target[2 * index + 1] = point.y
|
|
372
|
+
}
|
|
373
|
+
NativeTheme.homography(cover.corners, target)?.let { cover.view.imageMatrix = it }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
fun reveal(duration: Long, promise: Promise) {
|
|
377
|
+
val reveal = Reveal(duration, covers.toList(), promise)
|
|
378
|
+
if (reveal.covers.isEmpty()) { promise.resolve(Arguments.makeNativeMap(mapOf("revealed" to true))); return }
|
|
379
|
+
reveals.add(reveal)
|
|
380
|
+
// Fades start on the next complete frame, or after ten seconds at most.
|
|
381
|
+
if (revealDeadline == null) {
|
|
382
|
+
val timeout = Runnable { revealDeadline = null; startReveals() }
|
|
383
|
+
revealDeadline = timeout; handler.postDelayed(timeout, 10000)
|
|
384
|
+
}
|
|
385
|
+
sdk.triggerRepaint()
|
|
386
|
+
}
|
|
387
|
+
private fun startReveals() {
|
|
388
|
+
revealDeadline?.let { handler.removeCallbacks(it) }; revealDeadline = null
|
|
389
|
+
val pending = reveals.toList(); reveals.clear()
|
|
390
|
+
for (reveal in pending) {
|
|
391
|
+
for (cover in reveal.covers) {
|
|
392
|
+
if (cover.fading) continue
|
|
393
|
+
cover.fading = true
|
|
394
|
+
cover.view.animate().alpha(0f).setDuration(reveal.duration).setInterpolator(AccelerateDecelerateInterpolator())
|
|
395
|
+
.withEndAction { removeCover(cover) }.start()
|
|
396
|
+
}
|
|
397
|
+
settle(reveal)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
private fun removeCover(cover: Cover) {
|
|
401
|
+
cover.view.animate().cancel(); map.removeView(cover.view); covers.remove(cover)
|
|
402
|
+
for (reveal in settling.toList()) settle(reveal)
|
|
403
|
+
}
|
|
404
|
+
// A reveal resolves once every cover that existed when it was requested is gone.
|
|
405
|
+
private fun settle(reveal: Reveal) {
|
|
406
|
+
if (reveal.covers.any { it in covers }) { if (reveal !in settling) settling.add(reveal); return }
|
|
407
|
+
settling.remove(reveal)
|
|
408
|
+
reveal.promise?.resolve(Arguments.makeNativeMap(mapOf("revealed" to true))); reveal.promise = null
|
|
409
|
+
}
|
|
410
|
+
fun discardCovers() {
|
|
411
|
+
revealDeadline?.let { handler.removeCallbacks(it) }; revealDeadline = null
|
|
412
|
+
for (cover in covers.toList()) { cover.view.animate().cancel(); map.removeView(cover.view) }
|
|
413
|
+
covers.clear()
|
|
414
|
+
for (reveal in reveals + settling) { reveal.promise?.resolve(Arguments.makeNativeMap(mapOf("revealed" to true))); reveal.promise = null }
|
|
415
|
+
reveals.clear(); settling.clear()
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
fun applyThemeValues(style: Style, values: ReadableMap): Int {
|
|
419
|
+
if (themedToken != token) {
|
|
420
|
+
// Layers and original artwork belong to one style.
|
|
421
|
+
themedToken = token; themedLayers.clear(); themedImages.clear()
|
|
422
|
+
}
|
|
423
|
+
var applied = 0
|
|
424
|
+
for (group in listOf("paint", "layout")) {
|
|
425
|
+
if (!values.hasKey(group)) continue
|
|
426
|
+
val entries = values.getArray(group) ?: invalid()
|
|
427
|
+
if (entries.size() > 8192) invalid()
|
|
428
|
+
for (index in 0 until entries.size()) {
|
|
429
|
+
val entry = entries.getArray(index) ?: invalid()
|
|
430
|
+
if (entry.size() != 3 || entry.getType(0) != ReadableType.String || entry.getType(1) != ReadableType.String) invalid()
|
|
431
|
+
val identifier = entry.getString(0) ?: invalid()
|
|
432
|
+
val layer = themedLayers[identifier] ?: style.getLayer(identifier)?.also { themedLayers[identifier] = it } ?: continue
|
|
433
|
+
val property = entry.getString(1) ?: invalid()
|
|
434
|
+
val value = NativeTheme.value(entry.getDynamic(2))
|
|
435
|
+
layer.setProperties(if (group == "paint") PaintPropertyValue(property, value) else LayoutPropertyValue(property, value))
|
|
436
|
+
applied++
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (values.hasKey("images")) {
|
|
440
|
+
val images = values.getArray("images") ?: invalid()
|
|
441
|
+
if (images.size() > 64) invalid()
|
|
442
|
+
// Originals are read before any artwork is replaced, so tuples can share names.
|
|
443
|
+
for (index in 0 until images.size()) {
|
|
444
|
+
val entry = images.getArray(index) ?: invalid()
|
|
445
|
+
if (entry.size() != 4 || (0 until 3).any { entry.getType(it) != ReadableType.String } || entry.getType(3) != ReadableType.Number) invalid()
|
|
446
|
+
for (name in (0 until 3).map { entry.getString(it) ?: invalid() })
|
|
447
|
+
if (!themedImages.containsKey(name)) style.getImage(name)?.let { themedImages[name] = it }
|
|
448
|
+
}
|
|
449
|
+
for (index in 0 until images.size()) {
|
|
450
|
+
val entry = images.getArray(index) ?: invalid()
|
|
451
|
+
val from = themedImages[entry.getString(1)] ?: continue
|
|
452
|
+
val to = themedImages[entry.getString(2)] ?: continue
|
|
453
|
+
val t = entry.getDouble(3)
|
|
454
|
+
if (!t.isFinite() || t < 0 || t > 1) invalid()
|
|
455
|
+
style.addImage(entry.getString(0) ?: invalid(), NativeTheme.mix(from, to, t))
|
|
456
|
+
applied++
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (values.hasKey("light")) {
|
|
460
|
+
val light = values.getMap("light") ?: invalid()
|
|
461
|
+
val target = style.light
|
|
462
|
+
if (target != null) {
|
|
463
|
+
if (light.hasKey("color") && light.getType("color") == ReadableType.String) target.setColor(light.getString("color") ?: invalid())
|
|
464
|
+
if (light.hasKey("intensity") && light.getType("intensity") == ReadableType.Number) target.intensity = light.getDouble("intensity").toFloat()
|
|
465
|
+
if (light.hasKey("position") && light.getType("position") == ReadableType.Array) {
|
|
466
|
+
val position = light.getArray("position") ?: invalid()
|
|
467
|
+
if (position.size() == 3 && (0 until 3).all { position.getType(it) == ReadableType.Number })
|
|
468
|
+
target.position = Position(position.getDouble(0).toFloat(), position.getDouble(1).toFloat(), position.getDouble(2).toFloat())
|
|
469
|
+
}
|
|
470
|
+
applied++
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return applied
|
|
474
|
+
}
|
|
475
|
+
|
|
268
476
|
override fun onViewAttachedToWindow(view: View) { guarded { sampleLayout() } }
|
|
269
477
|
override fun onViewDetachedFromWindow(view: View) {
|
|
270
478
|
retire(); root.removeOnAttachStateChangeListener(this); surfaces.remove(id)
|
|
@@ -27,9 +27,35 @@ type MapColorScheme = NonNullable<TileflowNativeSourceOptions['colorScheme']>;
|
|
|
27
27
|
type MapView = TileflowNativeInitialView;
|
|
28
28
|
/** Portable composition inputs, independent from source acquisition. */
|
|
29
29
|
type MapInitialViewInputs = TileflowNativeInitialViewOptions;
|
|
30
|
+
/**
|
|
31
|
+
* How a theme change appears. With a `duration`, a snapshot of the current frame covers the map,
|
|
32
|
+
* follows the camera, and fades out once the new theme is drawn. The device's reduce-motion
|
|
33
|
+
* setting changes at once.
|
|
34
|
+
*/
|
|
35
|
+
type MapThemeTransition = Readonly<{
|
|
36
|
+
/** Cross-fade length in milliseconds, from 0 (the default, an immediate change) to 5000. */
|
|
37
|
+
duration?: number;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* A continuous blend of two to eight concrete themes of the map, shown instead of `theme`.
|
|
41
|
+
* `position` runs from 0 (the first theme) to `themes.length - 1`; 1.25 is a quarter of the way
|
|
42
|
+
* from the second theme to the third. Ground colours and widths follow the position; labels and
|
|
43
|
+
* icons cross-fade when the nearest theme changes.
|
|
44
|
+
*/
|
|
45
|
+
type MapThemeBlend = Readonly<{
|
|
46
|
+
themes: readonly string[];
|
|
47
|
+
position: number;
|
|
48
|
+
}>;
|
|
30
49
|
type MapSourceProps = Readonly<{
|
|
31
50
|
source: MapSource;
|
|
32
51
|
theme?: MapThemeSelection;
|
|
52
|
+
/** Cross-fades theme changes, including entering and leaving a blend. */
|
|
53
|
+
themeTransition?: MapThemeTransition;
|
|
54
|
+
/**
|
|
55
|
+
* Shows a blend of themes instead of `theme`. The first blend of a set of themes prepares one
|
|
56
|
+
* style that holds all of them; moving `position` later changes the map without loading.
|
|
57
|
+
*/
|
|
58
|
+
themeBlend?: MapThemeBlend;
|
|
33
59
|
}>;
|
|
34
60
|
type AllowedMapOptions = 'dragPan' | 'touchZoom' | 'doubleTapZoom' | 'doubleTapHoldZoom' | 'touchRotate' | 'touchPitch' | 'compass' | 'compassHiddenFacingNorth' | 'scaleBar';
|
|
35
61
|
type OwnedMapOptions = Exclude<keyof MapProps$1, AllowedMapOptions> | 'container' | 'styleURL' | 'styleUrl' | 'transformRequest' | 'requestTransform' | 'requestHeaders';
|
|
@@ -141,4 +167,4 @@ type MapCameraProps = Readonly<{
|
|
|
141
167
|
/** Complete Tileflow source, presentation, mount-stable camera and portable interaction inputs. */
|
|
142
168
|
type MapProps<TAnnotation extends TileflowAnnotation = TileflowAnnotation> = MapBaseProps & MapCameraProps & MapInteractionProps<TAnnotation>;
|
|
143
169
|
|
|
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,
|
|
170
|
+
export type { MapThemeTransition as A, 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, MapThemeBlend as x, MapThemeChangeEvent as y, MapThemeSelection as z };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ReactElement } from 'react';
|
|
2
2
|
import { TileflowAnnotation } from '@tileflow/interactions';
|
|
3
3
|
export { TileflowAnnotation, TileflowInteractionBinding, TileflowInteractionCoordinate, TileflowInteractionDiagnostic, TileflowPoiCategory, TileflowResolvedAnnotationTarget, TileflowResolvedPoiFeatureTarget } from '@tileflow/interactions';
|
|
4
|
-
import { d as MapProps } from './contract-
|
|
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
|
|
4
|
+
import { d as MapProps } from './contract-Bwl-gB-I.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 MapThemeBlend, y as MapThemeChangeEvent, z as MapThemeSelection, A as MapThemeTransition, M as MapView, a as MapViewChangeEvent } from './contract-Bwl-gB-I.js';
|
|
6
6
|
import '@maplibre/maplibre-react-native';
|
|
7
7
|
import 'react-native';
|
|
8
8
|
import '@tileflow/core/native';
|