@tileflow/react-native 0.1.0-alpha.0 → 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.
@@ -1,4 +1,4 @@
1
- import { M as MapView, a as MapViewChangeEvent, b as MapInitialViewInputs, c as MapCameraProps } from '../contract-DWs-lDPo.js';
1
+ import { M as MapView, a as MapViewChangeEvent, b as MapInitialViewInputs, c as MapCameraProps } from '../contract-Bwl-gB-I.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -1,4 +1,4 @@
1
- import { b as MapInitialViewInputs, M as MapView } from '../contract-DWs-lDPo.js';
1
+ import { b as MapInitialViewInputs, M as MapView } from '../contract-Bwl-gB-I.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -1,4 +1,4 @@
1
- import { y as MapThemeSelection, f as MapColorScheme } from '../contract-DWs-lDPo.js';
1
+ import { z as MapThemeSelection, f as MapColorScheme } from '../contract-Bwl-gB-I.js';
2
2
  import '@maplibre/maplibre-react-native';
3
3
  import 'react';
4
4
  import 'react-native';
@@ -1,5 +1,5 @@
1
1
  import { TileflowNativeSourceState } from '@tileflow/core/native';
2
- import { v as MapSourceState } from '../contract-DWs-lDPo.js';
2
+ import { v as MapSourceState } from '../contract-Bwl-gB-I.js';
3
3
  import '@maplibre/maplibre-react-native';
4
4
  import 'react';
5
5
  import 'react-native';
@@ -0,0 +1,28 @@
1
+ #import <QuartzCore/QuartzCore.h>
2
+ #import <UIKit/UIKit.h>
3
+
4
+ NS_ASSUME_NONNULL_BEGIN
5
+
6
+ // Pure helpers for theme transitions and blends on MapLibre iOS. UI-queue use only where noted.
7
+
8
+ /** The projective transform taking four points to four points (h[8] = 1), or NO when degenerate. */
9
+ FOUNDATION_EXPORT BOOL TFThemeHomography(const CGPoint from[_Nonnull 4], const CGPoint to[_Nonnull 4], double h[_Nonnull 9]);
10
+ /** A homography as a layer transform for anchor point (0, 0). */
11
+ FOUNDATION_EXPORT CATransform3D TFThemeTransform(const double h[_Nonnull 9]);
12
+ /** The MapLibre iOS key of a style-specification layer property. */
13
+ FOUNDATION_EXPORT NSString *TFThemeStyleKey(NSString *property);
14
+ /** A hex, rgb(a), hsl(a), or transparent colour, or nil. */
15
+ FOUNDATION_EXPORT UIColor *_Nullable TFThemeColor(NSString *text);
16
+ /** Whether every element is a number (not a boolean). */
17
+ FOUNDATION_EXPORT BOOL TFThemeNumbers(NSArray *array);
18
+ /** Whether a value can be set at run time: constants and expressions that read no feature data. */
19
+ FOUNDATION_EXPORT BOOL TFThemeCameraValue(id _Nullable value, NSUInteger depth);
20
+ /**
21
+ * A style-specification value as the expression MapLibre iOS expects for `key`; nil resets it.
22
+ * `supported` is NO, with nil, for a value MapLibre iOS cannot take at run time.
23
+ */
24
+ FOUNDATION_EXPORT NSExpression *_Nullable TFThemeExpression(id _Nullable value, NSString *key, BOOL *supported);
25
+ /** Pattern artwork mixed in premultiplied pixels: `from` at 1 - t plus `to` at t. */
26
+ FOUNDATION_EXPORT UIImage *TFThemeMixImage(UIImage *from, UIImage *to, double t);
27
+
28
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,198 @@
1
+ #import "TFNativeTheme.h"
2
+ #import <MapLibre/MapLibre.h>
3
+ #import <algorithm>
4
+ #import <cctype>
5
+ #import <cmath>
6
+
7
+ // The projective transform taking four points to four points (h[8] = 1), or NO when degenerate.
8
+ BOOL TFThemeHomography(const CGPoint from[4], const CGPoint to[4], double h[9]) {
9
+ double rows[8][9];
10
+ for (int i = 0; i < 4; i++) {
11
+ double x = from[i].x, y = from[i].y, u = to[i].x, v = to[i].y;
12
+ double first[9] = {x, y, 1, 0, 0, 0, -u * x, -u * y, u};
13
+ double second[9] = {0, 0, 0, x, y, 1, -v * x, -v * y, v};
14
+ for (int k = 0; k < 9; k++) { rows[2 * i][k] = first[k]; rows[2 * i + 1][k] = second[k]; }
15
+ }
16
+ for (int column = 0; column < 8; column++) {
17
+ int pivot = column;
18
+ for (int row = column + 1; row < 8; row++) if (std::fabs(rows[row][column]) > std::fabs(rows[pivot][column])) pivot = row;
19
+ if (std::fabs(rows[pivot][column]) < 1e-12) return NO;
20
+ if (pivot != column) for (int k = 0; k < 9; k++) std::swap(rows[pivot][k], rows[column][k]);
21
+ for (int row = 0; row < 8; row++) {
22
+ if (row == column) continue;
23
+ double factor = rows[row][column] / rows[column][column];
24
+ for (int k = column; k < 9; k++) rows[row][k] -= factor * rows[column][k];
25
+ }
26
+ }
27
+ for (int i = 0; i < 8; i++) { h[i] = rows[i][8] / rows[i][i]; if (!std::isfinite(h[i])) return NO; }
28
+ h[8] = 1;
29
+ return YES;
30
+ }
31
+
32
+ // MapLibre iOS names layer properties after Cocoa conventions; everything else is camel case.
33
+ NSString *TFThemeStyleKey(NSString *property) {
34
+ static NSDictionary<NSString *, NSString *> *renamed;
35
+ static dispatch_once_t once;
36
+ dispatch_once(&once, ^{
37
+ renamed = @{
38
+ @"circle-pitch-scale": @"circleScaleAlignment", @"circle-translate": @"circleTranslation", @"circle-translate-anchor": @"circleTranslationAnchor",
39
+ @"fill-antialias": @"fillAntialiased", @"fill-translate": @"fillTranslation", @"fill-translate-anchor": @"fillTranslationAnchor",
40
+ @"fill-extrusion-translate": @"fillExtrusionTranslation", @"fill-extrusion-translate-anchor": @"fillExtrusionTranslationAnchor",
41
+ @"fill-extrusion-vertical-gradient": @"fillExtrusionHasVerticalGradient", @"icon-allow-overlap": @"iconAllowsOverlap",
42
+ @"icon-ignore-placement": @"iconIgnoresPlacement", @"icon-image": @"iconImageName", @"icon-keep-upright": @"keepsIconUpright",
43
+ @"icon-rotate": @"iconRotation", @"icon-size": @"iconScale", @"icon-translate": @"iconTranslation", @"icon-translate-anchor": @"iconTranslationAnchor",
44
+ @"line-dasharray": @"lineDashPattern", @"line-translate": @"lineTranslation", @"line-translate-anchor": @"lineTranslationAnchor",
45
+ @"raster-brightness-max": @"maximumRasterBrightness", @"raster-brightness-min": @"minimumRasterBrightness",
46
+ @"raster-hue-rotate": @"rasterHueRotation", @"raster-resampling": @"rasterResamplingMode", @"symbol-avoid-edges": @"symbolAvoidsEdges",
47
+ @"text-allow-overlap": @"textAllowsOverlap", @"text-field": @"text", @"text-font": @"textFontNames", @"text-ignore-placement": @"textIgnoresPlacement",
48
+ @"text-justify": @"textJustification", @"text-keep-upright": @"keepsTextUpright", @"text-max-angle": @"maximumTextAngle",
49
+ @"text-max-width": @"maximumTextWidth", @"text-rotate": @"textRotation", @"text-size": @"textFontSize", @"text-translate": @"textTranslation",
50
+ @"text-translate-anchor": @"textTranslationAnchor", @"text-writing-mode": @"textWritingModes",
51
+ };
52
+ });
53
+ NSString *key = renamed[property];
54
+ if (key) return key;
55
+ NSMutableString *camel = [NSMutableString string];
56
+ BOOL upper = NO;
57
+ for (NSUInteger index = 0; index < property.length; index++) {
58
+ unichar character = [property characterAtIndex:index];
59
+ if (character == '-') { upper = YES; continue; }
60
+ [camel appendFormat:@"%C", upper ? (unichar)toupper(character) : character];
61
+ upper = NO;
62
+ }
63
+ return camel;
64
+ }
65
+ static BOOL TFThemeChannel(NSString *text, double scale, double *out) {
66
+ NSString *trimmed = [text stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet];
67
+ BOOL percentage = [trimmed hasSuffix:@"%"];
68
+ if (percentage) trimmed = [trimmed substringToIndex:trimmed.length - 1];
69
+ NSScanner *scanner = [NSScanner scannerWithString:trimmed];
70
+ double value;
71
+ if (![scanner scanDouble:&value] || !scanner.isAtEnd || !std::isfinite(value)) return NO;
72
+ *out = percentage ? value / 100.0 * scale : value;
73
+ return YES;
74
+ }
75
+ // Hex, rgb(a), and hsl(a) colours; nil for anything else, which MapLibre then parses itself.
76
+ UIColor *TFThemeColor(NSString *text) {
77
+ NSString *colour = [[text stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceCharacterSet] lowercaseString];
78
+ if ([colour isEqual:@"transparent"]) return [UIColor colorWithRed:0 green:0 blue:0 alpha:0];
79
+ if ([colour hasPrefix:@"#"]) {
80
+ NSString *hex = [colour substringFromIndex:1];
81
+ if (hex.length == 3 || hex.length == 4) {
82
+ NSMutableString *full = [NSMutableString string];
83
+ for (NSUInteger index = 0; index < hex.length; index++) { unichar digit = [hex characterAtIndex:index]; [full appendFormat:@"%C%C", digit, digit]; }
84
+ hex = full;
85
+ }
86
+ if (hex.length != 6 && hex.length != 8) return nil;
87
+ unsigned long long number = 0;
88
+ NSScanner *scanner = [NSScanner scannerWithString:hex];
89
+ if (![scanner scanHexLongLong:&number] || !scanner.isAtEnd) return nil;
90
+ if (hex.length == 6) number = (number << 8) | 0xff;
91
+ return [UIColor colorWithRed:((number >> 24) & 0xff) / 255.0 green:((number >> 16) & 0xff) / 255.0 blue:((number >> 8) & 0xff) / 255.0 alpha:(number & 0xff) / 255.0];
92
+ }
93
+ NSRange open = [colour rangeOfString:@"("];
94
+ if (open.location == NSNotFound || ![colour hasSuffix:@")"]) return nil;
95
+ NSString *function = [colour substringToIndex:open.location];
96
+ NSString *body = [colour substringWithRange:NSMakeRange(open.location + 1, colour.length - open.location - 2)];
97
+ NSMutableArray<NSString *> *parts = [NSMutableArray array];
98
+ for (NSString *part in [body componentsSeparatedByCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@", /"]]) if (part.length) [parts addObject:part];
99
+ if (parts.count < 3 || parts.count > 4) return nil;
100
+ double alpha = 1;
101
+ if (parts.count == 4 && !TFThemeChannel(parts[3], 1, &alpha)) return nil;
102
+ if ([function isEqual:@"rgb"] || [function isEqual:@"rgba"]) {
103
+ double red, green, blue;
104
+ if (!TFThemeChannel(parts[0], 255, &red) || !TFThemeChannel(parts[1], 255, &green) || !TFThemeChannel(parts[2], 255, &blue)) return nil;
105
+ return [UIColor colorWithRed:red / 255.0 green:green / 255.0 blue:blue / 255.0 alpha:alpha];
106
+ }
107
+ if ([function isEqual:@"hsl"] || [function isEqual:@"hsla"]) {
108
+ double hue, saturation, lightness;
109
+ if (!TFThemeChannel(parts[0], 1, &hue) || !TFThemeChannel(parts[1], 1, &saturation) || !TFThemeChannel(parts[2], 1, &lightness)) return nil;
110
+ double chroma = (1 - std::fabs(2 * lightness - 1)) * saturation;
111
+ double sector = std::fmod(std::fmod(hue, 360) + 360, 360) / 60;
112
+ double x = chroma * (1 - std::fabs(std::fmod(sector, 2) - 1));
113
+ double r = 0, g = 0, b = 0;
114
+ if (sector < 1) { r = chroma; g = x; } else if (sector < 2) { r = x; g = chroma; } else if (sector < 3) { g = chroma; b = x; }
115
+ else if (sector < 4) { g = x; b = chroma; } else if (sector < 5) { r = x; b = chroma; } else { r = chroma; b = x; }
116
+ double offset = lightness - chroma / 2;
117
+ return [UIColor colorWithRed:r + offset green:g + offset blue:b + offset alpha:alpha];
118
+ }
119
+ return nil;
120
+ }
121
+ BOOL TFThemeNumbers(NSArray *array) {
122
+ for (id item in array) if (![item isKindOfClass:NSNumber.class] || CFGetTypeID((__bridge CFTypeRef)item) == CFBooleanGetTypeID()) return NO;
123
+ return YES;
124
+ }
125
+ // MapLibre iOS converts expressions through NSExpression, which cannot round-trip every expression
126
+ // that reads feature data or combines conditions. Blend plans set only camera values at run time;
127
+ // anything else is left unchanged rather than risked.
128
+ BOOL TFThemeCameraValue(id value, NSUInteger depth) {
129
+ static NSSet<NSString *> *refused;
130
+ static dispatch_once_t once;
131
+ dispatch_once(&once, ^{
132
+ refused = [NSSet setWithArray:@[@"!", @"!has", @"!in", @"accumulated", @"all", @"any", @"distance", @"feature-state", @"geometry-type",
133
+ @"get", @"global-state", @"has", @"heatmap-density", @"id", @"in", @"line-progress", @"none", @"properties", @"within"]];
134
+ });
135
+ if (![value isKindOfClass:NSArray.class]) return YES;
136
+ if (depth > 32) return NO;
137
+ NSArray *array = value;
138
+ if (array.count && [array[0] isKindOfClass:NSString.class]) {
139
+ if ([refused containsObject:array[0]]) return NO;
140
+ if ([array[0] isEqual:@"literal"]) return YES;
141
+ }
142
+ for (id item in array) if (!TFThemeCameraValue(item, depth + 1)) return NO;
143
+ return YES;
144
+ }
145
+ // A style-specification value as the NSExpression MapLibre iOS expects for `key`; nil resets it.
146
+ NSExpression *TFThemeExpression(id value, NSString *key, BOOL *supported) {
147
+ *supported = YES;
148
+ if (!value || value == NSNull.null) return nil;
149
+ if ([value isKindOfClass:NSString.class]) {
150
+ if ([key hasSuffix:@"Color"]) {
151
+ // MapLibre iOS cannot cast a colour name at run time; such a value is left unchanged.
152
+ UIColor *colour = TFThemeColor(value);
153
+ *supported = colour != nil;
154
+ return colour ? [NSExpression expressionForConstantValue:colour] : nil;
155
+ }
156
+ return [NSExpression expressionForConstantValue:value];
157
+ }
158
+ if ([value isKindOfClass:NSNumber.class]) return [NSExpression expressionForConstantValue:value];
159
+ if ([value isKindOfClass:NSArray.class]) {
160
+ NSArray *array = value;
161
+ if (array.count == 2 && TFThemeNumbers(array) && ([key hasSuffix:@"Translation"] || [key hasSuffix:@"Offset"]))
162
+ return [NSExpression expressionForConstantValue:[NSValue valueWithCGVector:CGVectorMake([array[0] doubleValue], [array[1] doubleValue])]];
163
+ if (TFThemeNumbers(array)) return [NSExpression expressionForConstantValue:array];
164
+ BOOL strings = YES;
165
+ for (id item in array) strings = strings && [item isKindOfClass:NSString.class];
166
+ if (strings && [key isEqual:@"textFontNames"]) return [NSExpression expressionForConstantValue:array];
167
+ }
168
+ return [NSExpression expressionWithMLNJSONObject:value];
169
+ }
170
+ // Pattern artwork mixed in premultiplied pixels: `from` at 1 - t plus `to` at t.
171
+ UIImage *TFThemeMixImage(UIImage *from, UIImage *to, double t) {
172
+ if (t <= 0) return from;
173
+ if (t >= 1) return to;
174
+ CGImageRef a = from.CGImage, b = to.CGImage;
175
+ if (!a || !b || CGImageGetWidth(a) != CGImageGetWidth(b) || CGImageGetHeight(a) != CGImageGetHeight(b)) return t < 0.5 ? from : to;
176
+ size_t width = CGImageGetWidth(a), height = CGImageGetHeight(a);
177
+ CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
178
+ CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, width * 4, space, (CGBitmapInfo)kCGImageAlphaPremultipliedLast);
179
+ CGColorSpaceRelease(space);
180
+ if (!context) return t < 0.5 ? from : to;
181
+ CGRect rect = CGRectMake(0, 0, width, height);
182
+ CGContextSetAlpha(context, 1 - t); CGContextDrawImage(context, rect, a);
183
+ CGContextSetBlendMode(context, kCGBlendModePlusLighter); CGContextSetAlpha(context, t); CGContextDrawImage(context, rect, b);
184
+ CGImageRef mixed = CGBitmapContextCreateImage(context);
185
+ CGContextRelease(context);
186
+ if (!mixed) return t < 0.5 ? from : to;
187
+ UIImage *image = [UIImage imageWithCGImage:mixed scale:from.scale orientation:UIImageOrientationUp];
188
+ CGImageRelease(mixed);
189
+ return from.renderingMode == UIImageRenderingModeAlwaysTemplate ? [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] : image;
190
+ }
191
+ // The homography as a layer transform for anchor point (0, 0): row vectors, perspective in m14/m24.
192
+ CATransform3D TFThemeTransform(const double h[9]) {
193
+ CATransform3D transform = CATransform3DIdentity;
194
+ transform.m11 = h[0]; transform.m12 = h[3]; transform.m14 = h[6];
195
+ transform.m21 = h[1]; transform.m22 = h[4]; transform.m24 = h[7];
196
+ transform.m41 = h[2]; transform.m42 = h[5]; transform.m44 = h[8];
197
+ return transform;
198
+ }
@@ -5,8 +5,10 @@
5
5
  #import <MapLibreReactNative/MLRNMapView.h>
6
6
  #import <MapLibreReactNative/MLRNCamera.h>
7
7
  #import <QuartzCore/QuartzCore.h>
8
+ #import <algorithm>
8
9
  #import "TFAdmissionEngine.h"
9
10
  #import "TFNativeSurfaceState.h"
11
+ #import "TFNativeTheme.h"
10
12
  #import <cmath>
11
13
 
12
14
  static void TFSurfaceInvalid(void) {
@@ -67,6 +69,32 @@ static MLRNCamera *TFSurfaceCamera(UIView *root, MLRNMapView *map) {
67
69
  return found;
68
70
  }
69
71
 
72
+ // The view MapLibre draws into: the subview backed by a Metal (or OpenGL) layer.
73
+ static UIView *TFSurfaceRenderView(MLNMapView *map) {
74
+ for (UIView *view in map.subviews) {
75
+ NSString *name = NSStringFromClass(view.layer.class);
76
+ if ([name containsString:@"Metal"] || [name containsString:@"EAGL"]) return view;
77
+ }
78
+ return nil;
79
+ }
80
+ // A snapshot of one frame over the rendering view. It keeps four ground points of that frame and
81
+ // follows them with the ground plane's projective transform while the camera moves.
82
+ @interface TFSurfaceCover : NSObject
83
+ @property (nonatomic, strong) UIView *view;
84
+ @property (nonatomic, copy) NSArray<NSValue *> *corners;
85
+ @property (nonatomic, copy) NSArray<NSValue *> *anchors;
86
+ @property (nonatomic) BOOL fading;
87
+ @end
88
+ @implementation TFSurfaceCover
89
+ @end
90
+ @interface TFSurfaceReveal : NSObject
91
+ @property (nonatomic) double duration;
92
+ @property (nonatomic, strong) NSMutableArray<TFSurfaceCover *> *covers;
93
+ @property (nonatomic, copy) RCTPromiseResolveBlock resolve;
94
+ @end
95
+ @implementation TFSurfaceReveal
96
+ @end
97
+
70
98
  @interface TFSurfaceLayoutProbe : UIView
71
99
  @property (nonatomic, copy) dispatch_block_t changed;
72
100
  @end
@@ -90,8 +118,19 @@ static MLRNCamera *TFSurfaceCamera(UIView *root, MLRNMapView *map) {
90
118
  @property (nonatomic) BOOL retiring;
91
119
  @property (nonatomic) BOOL foreground;
92
120
  @property (nonatomic) BOOL observing;
121
+ @property (nonatomic, strong) NSMutableArray<TFSurfaceCover *> *covers;
122
+ @property (nonatomic, strong) NSMutableArray<TFSurfaceReveal *> *reveals;
123
+ @property (nonatomic, strong) NSMutableArray<TFSurfaceReveal *> *settling;
124
+ @property (nonatomic, copy, nullable) dispatch_block_t revealDeadline;
125
+ @property (nonatomic, copy, nullable) NSString *themedToken;
126
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, MLNStyleLayer *> *themedLayers;
127
+ @property (nonatomic, strong) NSMutableDictionary<NSString *, UIImage *> *themedImages;
93
128
  - (void)attach;
129
+ - (NSUInteger)applyThemeValues:(NSDictionary *)values;
94
130
  - (void)retire;
131
+ - (BOOL)cover;
132
+ - (void)reveal:(double)duration resolve:(RCTPromiseResolveBlock)resolve;
133
+ - (void)discardCovers;
95
134
  - (BOOL)owns;
96
135
  - (MLNStyle *)style;
97
136
  - (NSDictionary *)view;
@@ -114,6 +153,7 @@ static MLRNCamera *TFSurfaceCamera(UIView *root, MLRNMapView *map) {
114
153
  }
115
154
  - (void)attach {
116
155
  self.token = @""; self.detached = [NSMutableArray array]; self.previous = self.map.delegate;
156
+ self.covers = [NSMutableArray array]; self.reveals = [NSMutableArray array]; self.settling = [NSMutableArray array];
117
157
  if ([self.previous isKindOfClass:TFSurfaceAttachment.class]) TFSurfaceInvalid();
118
158
  self.map.delegate = self;
119
159
  [self.map addObserver:self forKeyPath:@"delegate" options:0 context:(__bridge void *)self]; self.observing = YES;
@@ -149,8 +189,182 @@ static MLRNCamera *TFSurfaceCamera(UIView *root, MLRNMapView *map) {
149
189
  for (NSUInteger index = 0; index < 4; index++) visible = visible && [snapshot[index] doubleValue] > 0;
150
190
  [self.state layout:visible];
151
191
  }
192
+ - (BOOL)cover {
193
+ if (UIAccessibilityIsReduceMotionEnabled() || !self.foreground || !self.root.window || !self.style) return NO;
194
+ for (TFSurfaceCover *existing in self.covers) if (!existing.fading) return YES;
195
+ UIView *render = TFSurfaceRenderView(self.map);
196
+ CGRect frame = render.frame;
197
+ if (!render || frame.size.width < 1 || frame.size.height < 1 || self.covers.count >= 4) return NO;
198
+ UIView *snapshot = [render snapshotViewAfterScreenUpdates:NO];
199
+ if (!snapshot) return NO;
200
+ snapshot.userInteractionEnabled = NO; snapshot.accessibilityElementsHidden = YES;
201
+ snapshot.layer.anchorPoint = CGPointZero; snapshot.frame = frame;
202
+ // Ground points below the horizon at any supported pitch, in map view coordinates.
203
+ CGFloat width = frame.size.width, height = frame.size.height;
204
+ NSMutableArray<NSValue *> *corners = [NSMutableArray array], *anchors = [NSMutableArray array];
205
+ for (NSValue *value in @[[NSValue valueWithCGPoint:CGPointMake(width * 0.1, height * 0.45)], [NSValue valueWithCGPoint:CGPointMake(width * 0.9, height * 0.45)],
206
+ [NSValue valueWithCGPoint:CGPointMake(width * 0.9, height * 0.9)], [NSValue valueWithCGPoint:CGPointMake(width * 0.1, height * 0.9)]]) {
207
+ CGPoint point = value.CGPointValue;
208
+ CLLocationCoordinate2D coordinate = [self.map convertPoint:CGPointMake(point.x + frame.origin.x, point.y + frame.origin.y) toCoordinateFromView:self.map];
209
+ if (!std::isfinite(coordinate.latitude) || !std::isfinite(coordinate.longitude) || std::fabs(coordinate.latitude) > 90) return NO;
210
+ [corners addObject:value];
211
+ [anchors addObject:[NSValue valueWithBytes:&coordinate objCType:@encode(CLLocationCoordinate2D)]];
212
+ }
213
+ TFSurfaceCover *cover = [TFSurfaceCover new];
214
+ cover.view = snapshot; cover.corners = corners; cover.anchors = anchors;
215
+ // Newer covers go below older ones, which keep fading on top.
216
+ [self.map insertSubview:snapshot aboveSubview:render];
217
+ [self.covers addObject:cover];
218
+ return YES;
219
+ }
220
+ - (void)followCovers {
221
+ if (!self.covers.count) return;
222
+ UIView *render = TFSurfaceRenderView(self.map);
223
+ if (!render) return;
224
+ CGPoint origin = render.frame.origin;
225
+ for (TFSurfaceCover *cover in self.covers) {
226
+ CGPoint from[4], to[4];
227
+ for (int index = 0; index < 4; index++) {
228
+ from[index] = cover.corners[index].CGPointValue;
229
+ CLLocationCoordinate2D coordinate;
230
+ [cover.anchors[index] getValue:&coordinate size:sizeof(coordinate)];
231
+ CGPoint point = [self.map convertCoordinate:coordinate toPointToView:self.map];
232
+ if (!std::isfinite(point.x) || !std::isfinite(point.y)) return;
233
+ to[index] = CGPointMake(point.x - origin.x, point.y - origin.y);
234
+ }
235
+ double h[9];
236
+ if (!TFThemeHomography(from, to, h)) continue;
237
+ [CATransaction begin]; [CATransaction setDisableActions:YES];
238
+ cover.view.layer.position = origin; cover.view.layer.transform = TFThemeTransform(h);
239
+ [CATransaction commit];
240
+ }
241
+ }
242
+ - (void)reveal:(double)duration resolve:(RCTPromiseResolveBlock)resolve {
243
+ TFSurfaceReveal *reveal = [TFSurfaceReveal new];
244
+ reveal.duration = duration; reveal.resolve = resolve; reveal.covers = [self.covers mutableCopy];
245
+ if (!reveal.covers.count) { resolve(@{@"revealed": @YES}); return; }
246
+ [self.reveals addObject:reveal];
247
+ // Fades start on the next complete frame, or after ten seconds at most.
248
+ if (!self.revealDeadline) {
249
+ __weak TFSurfaceAttachment *weakSelf = self;
250
+ dispatch_block_t deadline = dispatch_block_create((dispatch_block_flags_t)0, ^{ weakSelf.revealDeadline = nil; [weakSelf startReveals]; });
251
+ self.revealDeadline = deadline;
252
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC), dispatch_get_main_queue(), deadline);
253
+ }
254
+ [self.map triggerRepaint];
255
+ }
256
+ - (void)startReveals {
257
+ if (self.revealDeadline) { dispatch_block_cancel(self.revealDeadline); self.revealDeadline = nil; }
258
+ NSArray<TFSurfaceReveal *> *reveals = [self.reveals copy];
259
+ [self.reveals removeAllObjects];
260
+ for (TFSurfaceReveal *reveal in reveals) {
261
+ for (TFSurfaceCover *cover in reveal.covers) {
262
+ if (cover.fading) continue;
263
+ cover.fading = YES;
264
+ __weak TFSurfaceAttachment *weakSelf = self;
265
+ [UIView animateWithDuration:reveal.duration / 1000.0 delay:0 options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
266
+ animations:^{ cover.view.alpha = 0; } completion:^(__unused BOOL finished) { [weakSelf removeCover:cover]; }];
267
+ }
268
+ [self settle:reveal];
269
+ }
270
+ }
271
+ - (void)removeCover:(TFSurfaceCover *)cover {
272
+ [cover.view.layer removeAllAnimations]; [cover.view removeFromSuperview];
273
+ [self.covers removeObject:cover];
274
+ for (TFSurfaceReveal *reveal in [self.settling copy]) [self settle:reveal];
275
+ }
276
+ // A reveal resolves once every cover that existed when it was requested is gone.
277
+ - (void)settle:(TFSurfaceReveal *)reveal {
278
+ for (TFSurfaceCover *cover in reveal.covers) if ([self.covers containsObject:cover]) {
279
+ if (![self.settling containsObject:reveal]) [self.settling addObject:reveal];
280
+ return;
281
+ }
282
+ [self.settling removeObject:reveal];
283
+ if (reveal.resolve) { RCTPromiseResolveBlock resolve = reveal.resolve; reveal.resolve = nil; resolve(@{@"revealed": @YES}); }
284
+ }
285
+ - (void)discardCovers {
286
+ if (self.revealDeadline) { dispatch_block_cancel(self.revealDeadline); self.revealDeadline = nil; }
287
+ for (TFSurfaceCover *cover in [self.covers copy]) { [cover.view.layer removeAllAnimations]; [cover.view removeFromSuperview]; }
288
+ [self.covers removeAllObjects];
289
+ NSArray *reveals = [self.reveals arrayByAddingObjectsFromArray:self.settling];
290
+ [self.reveals removeAllObjects]; [self.settling removeAllObjects];
291
+ for (TFSurfaceReveal *reveal in reveals) if (reveal.resolve) { RCTPromiseResolveBlock resolve = reveal.resolve; reveal.resolve = nil; resolve(@{@"revealed": @YES}); }
292
+ }
293
+ - (NSUInteger)applyThemeValues:(NSDictionary *)values {
294
+ MLNStyle *style = self.style;
295
+ if (!style) TFSurfaceInvalid();
296
+ if (![self.themedToken isEqual:self.token]) {
297
+ // Layers and original artwork belong to one style.
298
+ self.themedToken = self.token; self.themedLayers = [NSMutableDictionary dictionary]; self.themedImages = [NSMutableDictionary dictionary];
299
+ }
300
+ NSUInteger applied = 0;
301
+ for (NSString *group in @[@"paint", @"layout"]) {
302
+ id entries = values[group];
303
+ if (!entries) continue;
304
+ if (![entries isKindOfClass:NSArray.class] || [entries count] > 8192) TFSurfaceInvalid();
305
+ for (id entry in entries) {
306
+ if (![entry isKindOfClass:NSArray.class] || [entry count] != 3 || ![entry[0] isKindOfClass:NSString.class] || ![entry[1] isKindOfClass:NSString.class]) TFSurfaceInvalid();
307
+ NSString *identifier = entry[0];
308
+ MLNStyleLayer *layer = self.themedLayers[identifier];
309
+ if (!layer) {
310
+ layer = [style layerWithIdentifier:identifier];
311
+ if (!layer) continue;
312
+ self.themedLayers[identifier] = layer;
313
+ }
314
+ if ([entry[1] isEqual:@"visibility"]) {
315
+ // Per-theme copies of a layer switch by visibility, a plain flag.
316
+ layer.visible = ![entry[2] isEqual:@"none"];
317
+ applied++;
318
+ continue;
319
+ }
320
+ NSString *key = TFThemeStyleKey(entry[1]);
321
+ if (![layer respondsToSelector:NSSelectorFromString(key)] || !TFThemeCameraValue(entry[2], 0)) continue;
322
+ // A value MapLibre rejects keeps its previous value; the rest of the batch still applies.
323
+ BOOL supported;
324
+ NSExpression *expression = TFThemeExpression(entry[2], key, &supported);
325
+ if (!supported) continue;
326
+ @try { [layer setValue:expression forKey:key]; applied++; }
327
+ @catch (__unused NSException *exception) {}
328
+ }
329
+ }
330
+ id images = values[@"images"];
331
+ if (images) {
332
+ if (![images isKindOfClass:NSArray.class] || [images count] > 64) TFSurfaceInvalid();
333
+ // Originals are read before any artwork is replaced, so tuples can share names.
334
+ for (NSArray *entry in images) {
335
+ if (![entry isKindOfClass:NSArray.class] || entry.count != 4) TFSurfaceInvalid();
336
+ for (NSUInteger index = 0; index < 3; index++) {
337
+ if (![entry[index] isKindOfClass:NSString.class]) TFSurfaceInvalid();
338
+ if (!self.themedImages[entry[index]]) { UIImage *image = [style imageForName:entry[index]]; if (image) self.themedImages[entry[index]] = image; }
339
+ }
340
+ }
341
+ for (NSArray *entry in images) {
342
+ UIImage *from = self.themedImages[entry[1]], *to = self.themedImages[entry[2]];
343
+ double t = TFSurfaceNumber(entry[3], 0, 1);
344
+ if (!from || !to) continue;
345
+ [style setImage:TFThemeMixImage(from, to, t) forName:entry[0]];
346
+ applied++;
347
+ }
348
+ }
349
+ id light = values[@"light"];
350
+ if (light) {
351
+ if (![light isKindOfClass:NSDictionary.class]) TFSurfaceInvalid();
352
+ MLNLight *next = style.light ?: [MLNLight new];
353
+ id colour = light[@"color"], intensity = light[@"intensity"], position = light[@"position"];
354
+ if ([colour isKindOfClass:NSString.class]) { UIColor *parsed = TFThemeColor(colour); if (parsed) next.color = [NSExpression expressionForConstantValue:parsed]; }
355
+ if ([intensity isKindOfClass:NSNumber.class]) next.intensity = [NSExpression expressionForConstantValue:intensity];
356
+ if ([position isKindOfClass:NSArray.class] && [position count] == 3 && TFThemeNumbers(position)) {
357
+ MLNSphericalPosition spherical = MLNSphericalPositionMake([position[0] doubleValue], [position[1] doubleValue], [position[2] doubleValue]);
358
+ next.position = [NSExpression expressionForConstantValue:[NSValue valueWithMLNSphericalPosition:spherical]];
359
+ }
360
+ style.light = next;
361
+ applied++;
362
+ }
363
+ return applied;
364
+ }
152
365
  - (void)retire {
153
366
  if (self.retiring) return;
367
+ [self discardCovers];
154
368
  self.retiring = YES; [self.state close];
155
369
  if (self.deadline) dispatch_block_cancel(self.deadline); self.deadline = nil;
156
370
  if (self.layoutCancel) self.layoutCancel(); self.layoutCancel = nil;
@@ -165,23 +379,23 @@ static MLRNCamera *TFSurfaceCamera(UIView *root, MLRNMapView *map) {
165
379
  if ([self.previous respondsToSelector:_cmd]) [self.previous mapView:mapView didFinishLoadingStyle:style];
166
380
  }
167
381
  - (void)mapViewWillStartRenderingFrame:(MLNMapView *)mapView {
168
- if ([self owns]) { [self sampleLayout]; [self.state frameStart:self.style]; }
382
+ if ([self owns]) { [self sampleLayout]; [self.state frameStart:self.style]; [self followCovers]; }
169
383
  if ([self.previous respondsToSelector:_cmd]) [self.previous mapViewWillStartRenderingFrame:mapView];
170
384
  }
171
385
  - (void)mapViewDidFinishRenderingMap:(MLNMapView *)mapView fullyRendered:(BOOL)fully {
172
386
  if ([self.previous respondsToSelector:_cmd]) [self.previous mapViewDidFinishRenderingMap:mapView fullyRendered:fully];
173
387
  }
174
388
  - (void)mapViewDidFinishRenderingFrame:(MLNMapView *)mapView fullyRendered:(BOOL)fully {
175
- if ([self owns]) [self.state frameEnd:self.style fully:fully];
389
+ if ([self owns]) { [self.state frameEnd:self.style fully:fully]; if (fully && self.reveals.count) [self startReveals]; }
176
390
  if ([self.previous respondsToSelector:_cmd]) [self.previous mapViewDidFinishRenderingFrame:mapView fullyRendered:fully];
177
391
  }
178
392
  - (void)mapViewDidFinishRenderingFrame:(MLNMapView *)mapView fullyRendered:(BOOL)fully frameEncodingTime:(double)encoding frameRenderingTime:(double)rendering {
179
- if ([self owns]) [self.state frameEnd:self.style fully:fully];
393
+ if ([self owns]) { [self.state frameEnd:self.style fully:fully]; if (fully && self.reveals.count) [self startReveals]; }
180
394
  if ([self.previous respondsToSelector:_cmd]) [self.previous mapViewDidFinishRenderingFrame:mapView fullyRendered:fully frameEncodingTime:encoding frameRenderingTime:rendering];
181
395
  else if ([self.previous respondsToSelector:@selector(mapViewDidFinishRenderingFrame:fullyRendered:)]) [self.previous mapViewDidFinishRenderingFrame:mapView fullyRendered:fully];
182
396
  }
183
397
  - (void)mapViewDidFinishRenderingFrame:(MLNMapView *)mapView fullyRendered:(BOOL)fully renderingStats:(MLNRenderingStats *)stats {
184
- if ([self owns]) [self.state frameEnd:self.style fully:fully];
398
+ if ([self owns]) { [self.state frameEnd:self.style fully:fully]; if (fully && self.reveals.count) [self startReveals]; }
185
399
  if ([self.previous respondsToSelector:_cmd]) [self.previous mapViewDidFinishRenderingFrame:mapView fullyRendered:fully renderingStats:stats];
186
400
  else if ([self.previous respondsToSelector:@selector(mapViewDidFinishRenderingFrame:fullyRendered:)]) [self.previous mapViewDidFinishRenderingFrame:mapView fullyRendered:fully];
187
401
  }
@@ -194,7 +408,7 @@ static MLRNCamera *TFSurfaceCamera(UIView *root, MLRNMapView *map) {
194
408
  else if ([self.previous respondsToSelector:@selector(mapView:regionWillChangeAnimated:)]) [self.previous mapView:mapView regionWillChangeAnimated:animated];
195
409
  }
196
410
  - (void)mapView:(MLNMapView *)mapView regionIsChangingWithReason:(MLNCameraChangeReason)reason {
197
- if ([self owns]) [self.state gestureChange:self.view];
411
+ if ([self owns]) { [self.state gestureChange:self.view]; [self followCovers]; }
198
412
  if ([self.previous respondsToSelector:_cmd]) [self.previous mapView:mapView regionIsChangingWithReason:reason];
199
413
  else if ([self.previous respondsToSelector:@selector(mapViewRegionIsChanging:)]) [self.previous mapViewRegionIsChanging:mapView];
200
414
  }
@@ -338,6 +552,40 @@ RCT_REMAP_METHOD(cancelCamera, cancelCameraForSurface:(NSString *)identifier seq
338
552
  @try { if (!TFAdmissionValidToken(identifier)) TFSurfaceInvalid(); [self.surfaces[identifier].state cancelCommand:TFSurfaceInteger(sequence)]; resolve(@{@"cancelled": @YES}); }
339
553
  @catch (NSException *exception) { TFSurfaceReject(reject); }
340
554
  }
555
+ static double TFSurfaceDuration(id value) {
556
+ if (![value isKindOfClass:NSNumber.class] || CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID()) TFSurfaceInvalid();
557
+ double number = [value doubleValue];
558
+ if (!std::isfinite(number) || number < 0 || number > 5000 || std::floor(number) != number) TFSurfaceInvalid();
559
+ return number;
560
+ }
561
+ RCT_REMAP_METHOD(coverSurface, coverSurface:(NSString *)identifier duration:(NSNumber *)duration resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
562
+ @try {
563
+ TFSurfaceAttachment *surface = [self current:identifier allowFailed:NO];
564
+ double milliseconds = TFSurfaceDuration(duration);
565
+ resolve(@{@"covered": @(milliseconds > 0 && [surface cover])});
566
+ } @catch (NSException *exception) { TFSurfaceReject(reject); }
567
+ }
568
+ RCT_REMAP_METHOD(revealSurface, revealSurface:(NSString *)identifier duration:(NSNumber *)duration resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
569
+ @try {
570
+ TFSurfaceAttachment *surface = [self current:identifier allowFailed:YES];
571
+ [surface reveal:TFSurfaceDuration(duration) resolve:resolve];
572
+ } @catch (NSException *exception) { TFSurfaceReject(reject); }
573
+ }
574
+ RCT_REMAP_METHOD(applyThemeValues, applyThemeValuesForSurface:(NSString *)identifier style:(NSString *)token values:(NSDictionary *)values resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
575
+ @try {
576
+ TFSurfaceAttachment *surface = [self current:identifier allowFailed:NO];
577
+ if (![values isKindOfClass:NSDictionary.class] || ![surface.token isEqual:token]) TFSurfaceInvalid();
578
+ NSUInteger applied = [surface applyThemeValues:values];
579
+ resolve(@{@"applied": @(applied)});
580
+ } @catch (NSException *exception) { TFSurfaceReject(reject); }
581
+ }
582
+ RCT_REMAP_METHOD(discardSurfaceCover, discardSurfaceCover:(NSString *)identifier resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
583
+ @try {
584
+ if (!TFAdmissionValidToken(identifier)) TFSurfaceInvalid();
585
+ [self.surfaces[identifier] discardCovers];
586
+ resolve(@{@"discarded": @YES});
587
+ } @catch (NSException *exception) { TFSurfaceReject(reject); }
588
+ }
341
589
  - (void)retire:(TFSurfaceAttachment *)surface resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject {
342
590
  [surface retire];
343
591
  if (!surface.root.window) { [self.surfaces removeObjectForKey:surface.identifier]; resolve(@{@"detached": @YES}); return; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tileflow/react-native",
3
- "version": "0.1.0-alpha.0",
3
+ "version": "0.1.0-alpha.2",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Tileflow maps, camera control and semantic interactions for React Native.",
6
6
  "type": "module",
@@ -36,7 +36,7 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@tileflow/core": ">=0.1.0-alpha.27 <0.1.0-beta.0",
39
+ "@tileflow/core": ">=0.1.0-alpha.31 <0.1.0-beta.0",
40
40
  "@tileflow/interactions": ">=0.1.0-alpha.4 <0.1.0-beta.0"
41
41
  },
42
42
  "peerDependencies": {