@superrouteca/smap-react-native 0.0.0-stage → 1.0.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/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/README.md +116 -2
- package/SMapReactNative.podspec +23 -0
- package/android/build.gradle +46 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/ca/superroute/smap/reactnative/SMapPackage.kt +17 -0
- package/android/src/main/java/ca/superroute/smap/reactnative/SMapReactView.kt +314 -0
- package/android/src/main/java/ca/superroute/smap/reactnative/SMapViewManager.kt +71 -0
- package/ios/RNSMapHost.swift +243 -0
- package/ios/RNSMapNativeView.mm +161 -0
- package/ios/smap_react_native.rb +96 -0
- package/package.json +67 -4
- package/src/SMap.tsx +226 -0
- package/src/SMapNativeViewNativeComponent.ts +47 -0
- package/src/geojson.ts +66 -0
- package/src/index.tsx +6 -0
- package/src/layers.tsx +114 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import CoreLocation
|
|
2
|
+
import SMap
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
/// The native side of `<SMap>` on iOS: holds one `SMapView`, created by `start` with the first props (the config is
|
|
6
|
+
/// read then; a new config arrives as a new view). The Fabric component view (`RNSMapNativeView`) forwards props and
|
|
7
|
+
/// commands here and turns `onEvent` calls into React Native events.
|
|
8
|
+
@objc(RNSMapHost)
|
|
9
|
+
public final class RNSMapHost: UIView {
|
|
10
|
+
/// Called with an event name (`mapReady`, `mapError`, `layerTap`, `tokenRequest`) and its payload.
|
|
11
|
+
@objc public var onEvent: ((String, [String: Any]) -> Void)?
|
|
12
|
+
|
|
13
|
+
private var map: SMapView?
|
|
14
|
+
private var appliedStyle = ""
|
|
15
|
+
private var appliedLanguage = ""
|
|
16
|
+
private var appliedCamera = ""
|
|
17
|
+
private var appliedBuildings = false
|
|
18
|
+
private var nextTokenRequest = 0
|
|
19
|
+
private var tokenRequests: [Int: CheckedContinuation<String, Error>] = [:]
|
|
20
|
+
|
|
21
|
+
@objc public var isStarted: Bool { map != nil }
|
|
22
|
+
|
|
23
|
+
/// Creates the map. `config`: JSON {baseUrl, nodes, apiKey, token, nearest, getToken}.
|
|
24
|
+
@objc public func start(config: String, style: String, language: String, camera: String, buildings: Bool) {
|
|
25
|
+
guard map == nil else { return }
|
|
26
|
+
let c = RNSMapHost.object(config)
|
|
27
|
+
let cam = RNSMapHost.object(camera)
|
|
28
|
+
var options = SMapOptions()
|
|
29
|
+
if let base = c["baseUrl"] as? String, let url = URL(string: base) { options.baseURL = url }
|
|
30
|
+
options.nodes = (c["nodes"] as? [String] ?? []).compactMap { URL(string: $0) }
|
|
31
|
+
options.token = (c["token"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
32
|
+
options.apiKey = (c["apiKey"] as? String).flatMap { $0.isEmpty ? nil : $0 }
|
|
33
|
+
options.nearest = c["nearest"] as? Bool ?? true
|
|
34
|
+
if c["getToken"] as? Bool == true {
|
|
35
|
+
options.getToken = { [weak self] in
|
|
36
|
+
guard let self = self else { throw SMapError.notReady }
|
|
37
|
+
return try await self.requestToken()
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
options.style = SMapStyle(rawValue: style) ?? .light
|
|
41
|
+
options.language = language.isEmpty ? nil : language
|
|
42
|
+
if let lat = RNSMapHost.number(cam["lat"]), let lng = RNSMapHost.number(cam["lng"]) {
|
|
43
|
+
options.center = CLLocationCoordinate2D(latitude: lat, longitude: lng)
|
|
44
|
+
}
|
|
45
|
+
options.zoom = RNSMapHost.number(cam["zoom"])
|
|
46
|
+
options.bearing = RNSMapHost.number(cam["bearing"]) ?? 0
|
|
47
|
+
options.pitch = RNSMapHost.number(cam["pitch"]) ?? 0
|
|
48
|
+
appliedStyle = style
|
|
49
|
+
appliedLanguage = language
|
|
50
|
+
appliedCamera = camera
|
|
51
|
+
appliedBuildings = buildings
|
|
52
|
+
|
|
53
|
+
let m = SMapView(frame: bounds, options: options)
|
|
54
|
+
m.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
55
|
+
m.onError = { [weak self] error in self?.emit("mapError", ["message": error.localizedDescription]) }
|
|
56
|
+
m.onReady = { [weak self] ready in
|
|
57
|
+
let node = ready.node.map { "\($0.scheme ?? "https")://\($0.host ?? "")" } ?? ""
|
|
58
|
+
self?.emit("mapReady", ["node": node])
|
|
59
|
+
}
|
|
60
|
+
addSubview(m)
|
|
61
|
+
map = m
|
|
62
|
+
if buildings { m.setBuildings3D(true) }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Applies props that changed since the last call.
|
|
66
|
+
@objc public func update(style: String, language: String, camera: String, buildings: Bool) {
|
|
67
|
+
guard let m = map else { return }
|
|
68
|
+
if style != appliedStyle {
|
|
69
|
+
appliedStyle = style
|
|
70
|
+
m.setStyle(SMapStyle(rawValue: style) ?? .light)
|
|
71
|
+
}
|
|
72
|
+
if language != appliedLanguage {
|
|
73
|
+
appliedLanguage = language
|
|
74
|
+
if !language.isEmpty { m.setLanguage(language) }
|
|
75
|
+
}
|
|
76
|
+
if camera != appliedCamera {
|
|
77
|
+
appliedCamera = camera
|
|
78
|
+
move(m, RNSMapHost.object(camera))
|
|
79
|
+
}
|
|
80
|
+
if buildings != appliedBuildings {
|
|
81
|
+
appliedBuildings = buildings
|
|
82
|
+
m.setBuildings3D(buildings)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private func move(_ m: SMapView, _ cam: [String: Any]) {
|
|
87
|
+
let engine = m.mapView
|
|
88
|
+
let camera = engine.camera
|
|
89
|
+
if let lat = RNSMapHost.number(cam["lat"]), let lng = RNSMapHost.number(cam["lng"]) {
|
|
90
|
+
camera.centerCoordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng)
|
|
91
|
+
}
|
|
92
|
+
if let bearing = RNSMapHost.number(cam["bearing"]) { camera.heading = bearing }
|
|
93
|
+
if let pitch = RNSMapHost.number(cam["pitch"]) { camera.pitch = CGFloat(pitch) }
|
|
94
|
+
engine.setCamera(camera, animated: false)
|
|
95
|
+
if let zoom = RNSMapHost.number(cam["zoom"]) { engine.setZoomLevel(zoom, animated: false) }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - Commands
|
|
99
|
+
|
|
100
|
+
@objc public func addLayer(kind: String, id: String, data: String, options: String) {
|
|
101
|
+
guard let m = map else { return }
|
|
102
|
+
let o = RNSMapHost.object(options)
|
|
103
|
+
let d = SMapData.geoJSON(Data(data.utf8))
|
|
104
|
+
do {
|
|
105
|
+
switch kind {
|
|
106
|
+
case "points": try m.addPoints(id, data: d, options: RNSMapHost.pointOptions(o))
|
|
107
|
+
case "lines": try m.addLines(id, data: d, options: RNSMapHost.lineOptions(o))
|
|
108
|
+
case "polygons": try m.addPolygons(id, data: d, options: RNSMapHost.polygonOptions(o))
|
|
109
|
+
default:
|
|
110
|
+
emit("mapError", ["message": "sMap: unknown layer kind \(kind)"])
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
m.onTap(id) { [weak self] properties, coordinate in
|
|
114
|
+
let json = (try? JSONSerialization.data(withJSONObject: RNSMapHost.jsonSafe(properties))).flatMap { String(data: $0, encoding: .utf8) } ?? "{}"
|
|
115
|
+
self?.emit("layerTap", ["layerId": id, "properties": json, "lat": coordinate.latitude, "lng": coordinate.longitude])
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
emit("mapError", ["message": error.localizedDescription])
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@objc public func setData(id: String, data: String) {
|
|
123
|
+
do { try map?.setData(id, data: .geoJSON(Data(data.utf8))) } catch { emit("mapError", ["message": error.localizedDescription]) }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@objc public func setVisible(id: String, visible: Bool) { map?.setVisible(id, visible) }
|
|
127
|
+
|
|
128
|
+
@objc public func removeLayer(id: String) { map?.removeLayer(id) }
|
|
129
|
+
|
|
130
|
+
@objc public func fitLayer(id: String, padding: Double, maxZoom: Double) { map?.fitLayer(id, padding: padding, maxZoom: maxZoom) }
|
|
131
|
+
|
|
132
|
+
@objc public func flyTo(lat: Double, lng: Double, zoom: Double) {
|
|
133
|
+
map?.flyTo(CLLocationCoordinate2D(latitude: lat, longitude: lng), zoom: zoom < 0 ? nil : zoom)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@objc public func resolveToken(requestId: Int, token: String, error: String) {
|
|
137
|
+
guard let c = tokenRequests.removeValue(forKey: requestId) else { return }
|
|
138
|
+
if token.isEmpty {
|
|
139
|
+
c.resume(throwing: SMapError.network(url: "", message: error.isEmpty ? "no token" : error))
|
|
140
|
+
} else {
|
|
141
|
+
c.resume(returning: token)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Ends the map and fails any token request still waiting.
|
|
146
|
+
@objc public func stop() {
|
|
147
|
+
for (_, c) in tokenRequests { c.resume(throwing: SMapError.notReady) }
|
|
148
|
+
tokenRequests.removeAll()
|
|
149
|
+
map?.removeFromSuperview()
|
|
150
|
+
map = nil
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func requestToken() async throws -> String {
|
|
154
|
+
nextTokenRequest += 1
|
|
155
|
+
let id = nextTokenRequest
|
|
156
|
+
return try await withCheckedThrowingContinuation { c in
|
|
157
|
+
tokenRequests[id] = c
|
|
158
|
+
emit("tokenRequest", ["requestId": id])
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private func emit(_ name: String, _ payload: [String: Any]) { onEvent?(name, payload) }
|
|
163
|
+
|
|
164
|
+
// MARK: - Options from JSON (the names every sMap SDK uses; `minzoom` / `maxzoom` as in the Web SDK too)
|
|
165
|
+
|
|
166
|
+
static func object(_ json: String) -> [String: Any] {
|
|
167
|
+
guard !json.isEmpty, let data = json.data(using: .utf8),
|
|
168
|
+
let o = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return [:] }
|
|
169
|
+
return o
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static func number(_ v: Any?) -> Double? {
|
|
173
|
+
guard let n = v as? NSNumber, !(v is Bool) else { return nil }
|
|
174
|
+
return n.doubleValue
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private static func zoom(_ o: [String: Any], _ name: String) -> Double? {
|
|
178
|
+
number(o[name]) ?? number(o[name.lowercased()])
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private static func color(_ v: Any?) -> UIColor? { (v as? String).map { UIColor(smapHex: $0) } }
|
|
182
|
+
|
|
183
|
+
private static func value(_ v: Any?, _ fallback: Double) -> SMapValue {
|
|
184
|
+
if let s = v as? String { return .property(s) }
|
|
185
|
+
return .number(number(v) ?? fallback)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static func pointOptions(_ o: [String: Any]) -> SMapPointOptions {
|
|
189
|
+
var p = SMapPointOptions()
|
|
190
|
+
if let c = o["cluster"] as? [String: Any] {
|
|
191
|
+
p.cluster = SMapCluster(radius: number(c["radius"]) ?? 50, maxZoom: number(c["maxZoom"]) ?? 14)
|
|
192
|
+
} else if o["cluster"] as? Bool == true {
|
|
193
|
+
p.cluster = SMapCluster()
|
|
194
|
+
}
|
|
195
|
+
if let c = color(o["color"]) { p.color = c }
|
|
196
|
+
if let r = number(o["radius"]) { p.radius = r }
|
|
197
|
+
if let c = color(o["strokeColor"]) { p.strokeColor = c }
|
|
198
|
+
if let w = number(o["strokeWidth"]) { p.strokeWidth = w }
|
|
199
|
+
p.icon = o["icon"] as? String
|
|
200
|
+
if let s = number(o["iconSize"]) { p.iconSize = s }
|
|
201
|
+
p.label = o["label"] as? String
|
|
202
|
+
p.minZoom = zoom(o, "minZoom")
|
|
203
|
+
p.maxZoom = zoom(o, "maxZoom")
|
|
204
|
+
p.zIndex = Int(number(o["zIndex"]) ?? 0)
|
|
205
|
+
return p
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
static func lineOptions(_ o: [String: Any]) -> SMapLineOptions {
|
|
209
|
+
var l = SMapLineOptions()
|
|
210
|
+
if let c = color(o["color"]) { l.color = c }
|
|
211
|
+
if let w = number(o["width"]) { l.width = w }
|
|
212
|
+
l.dash = (o["dash"] as? [NSNumber])?.map { $0.doubleValue }
|
|
213
|
+
if let a = number(o["opacity"]) { l.opacity = a }
|
|
214
|
+
l.minZoom = zoom(o, "minZoom")
|
|
215
|
+
l.maxZoom = zoom(o, "maxZoom")
|
|
216
|
+
l.zIndex = Int(number(o["zIndex"]) ?? 0)
|
|
217
|
+
return l
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
static func polygonOptions(_ o: [String: Any]) -> SMapPolygonOptions {
|
|
221
|
+
var p = SMapPolygonOptions()
|
|
222
|
+
if let c = color(o["fill"]) ?? color(o["color"]) { p.fill = c }
|
|
223
|
+
p.opacity = number(o["opacity"])
|
|
224
|
+
p.outline = color(o["outline"])
|
|
225
|
+
if let w = number(o["outlineWidth"]) { p.outlineWidth = w }
|
|
226
|
+
if let e = o["extrude"] as? [String: Any] {
|
|
227
|
+
p.extrude = SMapExtrude(height: value(e["height"], 20), base: value(e["base"], 0))
|
|
228
|
+
}
|
|
229
|
+
p.minZoom = zoom(o, "minZoom")
|
|
230
|
+
p.maxZoom = zoom(o, "maxZoom")
|
|
231
|
+
p.zIndex = Int(number(o["zIndex"]) ?? 0)
|
|
232
|
+
return p
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private static func jsonSafe(_ value: Any) -> Any {
|
|
236
|
+
switch value {
|
|
237
|
+
case let d as [String: Any]: return d.mapValues { jsonSafe($0) }
|
|
238
|
+
case let a as [Any]: return a.map { jsonSafe($0) }
|
|
239
|
+
case is String, is NSNumber, is NSNull: return value
|
|
240
|
+
default: return String(describing: value)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// The Fabric component view of `SMapNativeView`: forwards props and commands to the sMap iOS SDK (through RNSMapHost)
|
|
2
|
+
// and its events to React Native. It has no public header: React Native finds it by name (codegenConfig
|
|
3
|
+
// componentProvider), and a header importing the C++ component headers would break the pod's Swift module.
|
|
4
|
+
#import <React/RCTViewComponentView.h>
|
|
5
|
+
#import <UIKit/UIKit.h>
|
|
6
|
+
|
|
7
|
+
#import <react/renderer/components/SMapReactNativeSpec/ComponentDescriptors.h>
|
|
8
|
+
#import <react/renderer/components/SMapReactNativeSpec/EventEmitters.h>
|
|
9
|
+
#import <react/renderer/components/SMapReactNativeSpec/Props.h>
|
|
10
|
+
#import <react/renderer/components/SMapReactNativeSpec/RCTComponentViewHelpers.h>
|
|
11
|
+
|
|
12
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
13
|
+
|
|
14
|
+
#if __has_include(<SMapReactNative/SMapReactNative-Swift.h>)
|
|
15
|
+
#import <SMapReactNative/SMapReactNative-Swift.h>
|
|
16
|
+
#else
|
|
17
|
+
#import "SMapReactNative-Swift.h"
|
|
18
|
+
#endif
|
|
19
|
+
|
|
20
|
+
using namespace facebook::react;
|
|
21
|
+
|
|
22
|
+
static NSString *RNSMapString(const std::string &value)
|
|
23
|
+
{
|
|
24
|
+
return [NSString stringWithUTF8String:value.c_str()] ?: @"";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static std::string RNSMapStd(id value)
|
|
28
|
+
{
|
|
29
|
+
NSString *s = [value isKindOfClass:[NSString class]] ? value : @"";
|
|
30
|
+
return std::string([s UTF8String] ?: "");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static double RNSMapDouble(id value)
|
|
34
|
+
{
|
|
35
|
+
return [value isKindOfClass:[NSNumber class]] ? [value doubleValue] : 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@interface RNSMapNativeView : RCTViewComponentView <RCTSMapNativeViewViewProtocol>
|
|
39
|
+
@end
|
|
40
|
+
|
|
41
|
+
@implementation RNSMapNativeView {
|
|
42
|
+
RNSMapHost *_host;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
46
|
+
{
|
|
47
|
+
return concreteComponentDescriptorProvider<SMapNativeViewComponentDescriptor>();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
51
|
+
{
|
|
52
|
+
if (self = [super initWithFrame:frame]) {
|
|
53
|
+
static const auto defaultProps = std::make_shared<const SMapNativeViewProps>();
|
|
54
|
+
_props = defaultProps;
|
|
55
|
+
[self makeHost];
|
|
56
|
+
}
|
|
57
|
+
return self;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
- (void)makeHost
|
|
61
|
+
{
|
|
62
|
+
_host = [[RNSMapHost alloc] initWithFrame:self.bounds];
|
|
63
|
+
__weak RNSMapNativeView *weakSelf = self;
|
|
64
|
+
_host.onEvent = ^(NSString *name, NSDictionary<NSString *, id> *payload) {
|
|
65
|
+
[weakSelf emit:name payload:payload];
|
|
66
|
+
};
|
|
67
|
+
self.contentView = _host;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
71
|
+
{
|
|
72
|
+
const auto &p = *std::static_pointer_cast<SMapNativeViewProps const>(props);
|
|
73
|
+
NSString *style = RNSMapString(p.mapStyle);
|
|
74
|
+
NSString *language = RNSMapString(p.language);
|
|
75
|
+
NSString *camera = RNSMapString(p.camera);
|
|
76
|
+
if (!_host.isStarted) {
|
|
77
|
+
[_host startWithConfig:RNSMapString(p.config) style:style language:language camera:camera buildings:p.buildings3D];
|
|
78
|
+
} else {
|
|
79
|
+
[_host updateWithStyle:style language:language camera:camera buildings:p.buildings3D];
|
|
80
|
+
}
|
|
81
|
+
[super updateProps:props oldProps:oldProps];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
- (void)prepareForRecycle
|
|
85
|
+
{
|
|
86
|
+
// A recycled view may be given another config: start again from nothing.
|
|
87
|
+
[_host stop];
|
|
88
|
+
[self makeHost];
|
|
89
|
+
[super prepareForRecycle];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
- (void)emit:(NSString *)name payload:(NSDictionary<NSString *, id> *)payload
|
|
93
|
+
{
|
|
94
|
+
if (!_eventEmitter) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
auto emitter = std::static_pointer_cast<const SMapNativeViewEventEmitter>(_eventEmitter);
|
|
98
|
+
if ([name isEqualToString:@"mapReady"]) {
|
|
99
|
+
emitter->onMapReady({.node = RNSMapStd(payload[@"node"])});
|
|
100
|
+
} else if ([name isEqualToString:@"mapError"]) {
|
|
101
|
+
emitter->onMapError({.message = RNSMapStd(payload[@"message"])});
|
|
102
|
+
} else if ([name isEqualToString:@"layerTap"]) {
|
|
103
|
+
emitter->onLayerTap({
|
|
104
|
+
.layerId = RNSMapStd(payload[@"layerId"]),
|
|
105
|
+
.properties = RNSMapStd(payload[@"properties"]),
|
|
106
|
+
.lat = RNSMapDouble(payload[@"lat"]),
|
|
107
|
+
.lng = RNSMapDouble(payload[@"lng"]),
|
|
108
|
+
});
|
|
109
|
+
} else if ([name isEqualToString:@"tokenRequest"]) {
|
|
110
|
+
emitter->onTokenRequest({.requestId = (int)RNSMapDouble(payload[@"requestId"])});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#pragma mark - Commands
|
|
115
|
+
|
|
116
|
+
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
|
|
117
|
+
{
|
|
118
|
+
RCTSMapNativeViewHandleCommand(self, commandName, args);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
- (void)addLayer:(NSString *)kind id:(NSString *)layerId data:(NSString *)data options:(NSString *)options
|
|
122
|
+
{
|
|
123
|
+
[_host addLayerWithKind:kind id:layerId data:data options:options];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
- (void)setData:(NSString *)layerId data:(NSString *)data
|
|
127
|
+
{
|
|
128
|
+
[_host setDataWithId:layerId data:data];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
- (void)setVisible:(NSString *)layerId visible:(BOOL)visible
|
|
132
|
+
{
|
|
133
|
+
[_host setVisibleWithId:layerId visible:visible];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
- (void)removeLayer:(NSString *)layerId
|
|
137
|
+
{
|
|
138
|
+
[_host removeLayerWithId:layerId];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
- (void)fitLayer:(NSString *)layerId padding:(double)padding maxZoom:(double)maxZoom
|
|
142
|
+
{
|
|
143
|
+
[_host fitLayerWithId:layerId padding:padding maxZoom:maxZoom];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
- (void)flyTo:(double)lat lng:(double)lng zoom:(double)zoom
|
|
147
|
+
{
|
|
148
|
+
[_host flyToLat:lat lng:lng zoom:zoom];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
- (void)resolveToken:(NSInteger)requestId token:(NSString *)token error:(NSString *)error
|
|
152
|
+
{
|
|
153
|
+
[_host resolveTokenWithRequestId:requestId token:token error:error];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@end
|
|
157
|
+
|
|
158
|
+
Class<RCTComponentViewProtocol> RNSMapNativeViewCls(void)
|
|
159
|
+
{
|
|
160
|
+
return RNSMapNativeView.class;
|
|
161
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# CocoaPods helpers of @superrouteca/smap-react-native.
|
|
2
|
+
#
|
|
3
|
+
# The sMap iOS SDK is a Swift package (product `SMap`) whose rendering engine is a dynamic binary framework of that
|
|
4
|
+
# package. The pod gets the package through React Native's spm_dependency, which links it, but Xcode embeds a
|
|
5
|
+
# package's binary frameworks only in an app target that depends on the package itself. Linking the package into the
|
|
6
|
+
# app as well would link the SDK twice, so the app target is given a build phase that embeds the engine framework,
|
|
7
|
+
# by `SMapReactNative.post_install(installer)` in the app's Podfile:
|
|
8
|
+
#
|
|
9
|
+
# require Pod::Executable.execute_command('node', ['-p',
|
|
10
|
+
# 'require.resolve("@superrouteca/smap-react-native/ios/smap_react_native.rb", {paths: [process.argv[1]]})',
|
|
11
|
+
# __dir__]).strip
|
|
12
|
+
# ...
|
|
13
|
+
# post_install do |installer|
|
|
14
|
+
# react_native_post_install(installer, ...)
|
|
15
|
+
# SMapReactNative.post_install(installer)
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Which package is used: SMAP_IOS_PACKAGE_PATH (a checkout of the Swift package) when set; else, inside the sMap SDK
|
|
19
|
+
# repository, the package next to this one; else the tag of this release from GitHub.
|
|
20
|
+
require "json"
|
|
21
|
+
|
|
22
|
+
module SMapReactNative
|
|
23
|
+
REMOTE = "https://github.com/Goodneo-Solutions/smap-sdk.git".freeze
|
|
24
|
+
PRODUCT = "SMap".freeze
|
|
25
|
+
PHASE = "[sMap] Embed map engine".freeze
|
|
26
|
+
ENGINE = "MapLibre.framework".freeze
|
|
27
|
+
|
|
28
|
+
# Copies the engine framework from the build products (where Xcode puts the package's binary framework) into the
|
|
29
|
+
# app's Frameworks folder and signs it as the app is signed.
|
|
30
|
+
EMBED_SCRIPT = <<~SH.freeze
|
|
31
|
+
set -e
|
|
32
|
+
src="${BUILT_PRODUCTS_DIR}/#{ENGINE}"
|
|
33
|
+
dst="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
|
|
34
|
+
if [ ! -d "$src" ]; then
|
|
35
|
+
echo "error: #{ENGINE} not found in ${BUILT_PRODUCTS_DIR}; run pod install again"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
mkdir -p "$dst"
|
|
39
|
+
rm -rf "$dst/#{ENGINE}"
|
|
40
|
+
cp -R "$src" "$dst/"
|
|
41
|
+
if [ "${CODE_SIGNING_ALLOWED}" = "YES" ] && [ -n "${EXPANDED_CODE_SIGN_IDENTITY}" ]; then
|
|
42
|
+
codesign --force --sign "${EXPANDED_CODE_SIGN_IDENTITY}" --preserve-metadata=identifier,entitlements "$dst/#{ENGINE}"
|
|
43
|
+
fi
|
|
44
|
+
SH
|
|
45
|
+
|
|
46
|
+
def self.package_root
|
|
47
|
+
File.realpath(File.join(__dir__, ".."))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.version
|
|
51
|
+
JSON.parse(File.read(File.join(package_root, "package.json")))["version"]
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The local Swift package directory, or nil for the published one.
|
|
55
|
+
def self.local_sdk
|
|
56
|
+
env = ENV["SMAP_IOS_PACKAGE_PATH"]
|
|
57
|
+
return File.expand_path(env) if env && !env.empty?
|
|
58
|
+
[File.join(package_root, "..", "ios"), File.join(package_root, "..")].map { |d| File.expand_path(d) }.find do |dir|
|
|
59
|
+
manifest = File.join(dir, "Package.swift")
|
|
60
|
+
File.exist?(manifest) && File.read(manifest).include?(".library(name: \"#{PRODUCT}\"")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Called by the podspec.
|
|
65
|
+
def self.add_to_pod(spec)
|
|
66
|
+
local = local_sdk
|
|
67
|
+
if local
|
|
68
|
+
spm_dependency(spec, url: local, requirement: {}, products: [PRODUCT])
|
|
69
|
+
else
|
|
70
|
+
spm_dependency(spec, url: REMOTE, requirement: { kind: "exactVersion", version: version }, products: [PRODUCT])
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Adds the embed phase to every app target of the Podfile (once; kept up to date).
|
|
75
|
+
def self.post_install(installer)
|
|
76
|
+
inputs = ["${BUILT_PRODUCTS_DIR}/#{ENGINE}"]
|
|
77
|
+
outputs = ["${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/#{ENGINE}"]
|
|
78
|
+
installer.aggregate_targets.each do |aggregate|
|
|
79
|
+
project = aggregate.user_project
|
|
80
|
+
next unless project
|
|
81
|
+
changed = false
|
|
82
|
+
aggregate.user_targets.each do |target|
|
|
83
|
+
next unless target.respond_to?(:product_type) && target.product_type == "com.apple.product-type.application"
|
|
84
|
+
phase = target.shell_script_build_phases.find { |p| p.name == PHASE } || target.new_shell_script_build_phase(PHASE)
|
|
85
|
+
next if phase.shell_script == EMBED_SCRIPT && phase.input_paths == inputs && phase.output_paths == outputs
|
|
86
|
+
phase.shell_path = "/bin/sh"
|
|
87
|
+
phase.shell_script = EMBED_SCRIPT
|
|
88
|
+
phase.input_paths = inputs
|
|
89
|
+
phase.output_paths = outputs
|
|
90
|
+
Pod::UI.puts "[sMap] #{target.name} embeds the map engine framework"
|
|
91
|
+
changed = true
|
|
92
|
+
end
|
|
93
|
+
project.save if changed
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
package/package.json
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superrouteca/smap-react-native",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "React Native map view for Superroute's sMap service, over the sMap Android and iOS SDKs.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Goodneo Solutions",
|
|
7
|
+
"homepage": "https://github.com/Goodneo-Solutions/smap-sdk/tree/main/react-native#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/Goodneo-Solutions/smap-sdk.git",
|
|
11
|
+
"directory": "react-native"
|
|
12
|
+
},
|
|
13
|
+
"keywords": ["react-native", "map", "maps", "smap", "superroute", "ios", "android"],
|
|
14
|
+
"main": "src/index.tsx",
|
|
15
|
+
"types": "src/index.tsx",
|
|
16
|
+
"react-native": "src/index.tsx",
|
|
17
|
+
"source": "src/index.tsx",
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"!src/__tests__",
|
|
21
|
+
"android/build.gradle",
|
|
22
|
+
"android/src",
|
|
23
|
+
"ios",
|
|
24
|
+
"SMapReactNative.podspec",
|
|
25
|
+
"README.md",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "jest",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"prepack": "cp ../LICENSE LICENSE"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": ">=18",
|
|
36
|
+
"react-native": ">=0.81"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@babel/core": "7.29.7",
|
|
40
|
+
"@react-native/babel-preset": "0.88.0",
|
|
41
|
+
"@react-native/jest-preset": "0.88.0",
|
|
42
|
+
"@types/jest": "29.5.14",
|
|
43
|
+
"@types/react": "19.3.0",
|
|
44
|
+
"@types/react-test-renderer": "19.3.0",
|
|
45
|
+
"jest": "29.7.0",
|
|
46
|
+
"react": "19.3.0",
|
|
47
|
+
"react-native": "0.88.0",
|
|
48
|
+
"react-test-renderer": "19.3.0",
|
|
49
|
+
"typescript": "6.0.3"
|
|
50
|
+
},
|
|
51
|
+
"jest": {
|
|
52
|
+
"preset": "@react-native/jest-preset",
|
|
53
|
+
"roots": ["<rootDir>/src"],
|
|
54
|
+
"modulePathIgnorePatterns": ["<rootDir>/example/"]
|
|
55
|
+
},
|
|
56
|
+
"codegenConfig": {
|
|
57
|
+
"name": "SMapReactNativeSpec",
|
|
58
|
+
"type": "components",
|
|
59
|
+
"jsSrcsDir": "src",
|
|
60
|
+
"android": {
|
|
61
|
+
"javaPackageName": "ca.superroute.smap.reactnative"
|
|
62
|
+
},
|
|
63
|
+
"ios": {
|
|
64
|
+
"componentProvider": {
|
|
65
|
+
"SMapNativeView": "RNSMapNativeView"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|