diff --git a/__tests__/interface.test.js b/__tests__/interface.test.js index bdb85b0bf..141defc77 100644 --- a/__tests__/interface.test.js +++ b/__tests__/interface.test.js @@ -22,6 +22,7 @@ describe('Public Interface', () => { // modules 'offlineManager', + 'TileStore', 'offlineManagerLegacy', 'OfflineCreatePackOptions', 'snapshotManager', diff --git a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt index 3b5d91208..0e16f56a3 100644 --- a/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt +++ b/android/src/main/java/com/rnmapbox/rnmbx/RNMBXPackage.kt @@ -50,6 +50,7 @@ import com.rnmapbox.rnmbx.modules.RNMBXModule import com.rnmapbox.rnmbx.modules.RNMBXOfflineModule import com.rnmapbox.rnmbx.modules.RNMBXOfflineModuleLegacy import com.rnmapbox.rnmbx.modules.RNMBXSnapshotModule +import com.rnmapbox.rnmbx.modules.RNMBXTileStoreModule import com.rnmapbox.rnmbx.shape_animators.RNMBXMovePointShapeAnimatorModule import com.rnmapbox.rnmbx.shape_animators.ShapeAnimatorManager import com.rnmapbox.rnmbx.utils.ViewTagResolver @@ -90,6 +91,7 @@ class RNMBXPackage : TurboReactPackage() { RNMBXModule.REACT_CLASS -> return RNMBXModule(reactApplicationContext) RNMBXLocationModule.REACT_CLASS -> return RNMBXLocationModule(reactApplicationContext) RNMBXOfflineModule.REACT_CLASS -> return RNMBXOfflineModule(reactApplicationContext) + RNMBXTileStoreModule.REACT_CLASS -> return RNMBXTileStoreModule(reactApplicationContext) RNMBXOfflineModuleLegacy.REACT_CLASS -> return RNMBXOfflineModuleLegacy(reactApplicationContext) RNMBXSnapshotModule.REACT_CLASS -> return RNMBXSnapshotModule(reactApplicationContext) RNMBXLogging.REACT_CLASS -> return RNMBXLogging(reactApplicationContext) @@ -189,6 +191,15 @@ class RNMBXPackage : TurboReactPackage() { false, // isCxxModule false // isTurboModule ) + moduleInfos[RNMBXTileStoreModule.REACT_CLASS] = ReactModuleInfo( + RNMBXTileStoreModule.REACT_CLASS, + RNMBXTileStoreModule.REACT_CLASS, + false, // canOverrideExistingModule + false, // needsEagerInit + true, // hasConstants + false, // isCxxModule + false // isTurboModule + ) moduleInfos[RNMBXOfflineModuleLegacy.REACT_CLASS] = ReactModuleInfo( RNMBXOfflineModuleLegacy.REACT_CLASS, RNMBXOfflineModuleLegacy.REACT_CLASS, diff --git a/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXTileStoreModule.kt b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXTileStoreModule.kt new file mode 100644 index 000000000..29fbaa5fc --- /dev/null +++ b/android/src/main/java/com/rnmapbox/rnmbx/modules/RNMBXTileStoreModule.kt @@ -0,0 +1,68 @@ +package com.rnmapbox.rnmbx.modules + +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.module.annotations.ReactModule +import com.mapbox.common.TileDataDomain +import com.mapbox.common.TileStore +import com.rnmapbox.rnmbx.utils.extensions.toValue +import com.rnmapbox.rnmbx.utils.writableMapOf + +typealias Tag = Int + +@ReactModule(name = RNMBXTileStoreModule.REACT_CLASS) +class RNMBXTileStoreModule(private val mReactContext: ReactApplicationContext) : + ReactContextBaseJavaModule( + mReactContext + ) { + + fun shared(path: String?): TileStore { + return if (path != null) { + TileStore.create(path) + } else { + TileStore.create() + } + } + + @ReactMethod + fun shared(path: String?, promise: Promise) { + val tag = RNMBXTileStoreModule.tileStorePathTags.get(path) + if (tag != null) { + promise.resolve(tag) + } else { + val tileStore = shared(path) + RNMBXTileStoreModule.lastTag += 1 + val tag = RNMBXTileStoreModule.lastTag + RNMBXTileStoreModule.tileStores.put(tag, tileStore) + RNMBXTileStoreModule.tileStorePathTags.set(path, tag) + promise.resolve(tag) + } + } + + @ReactMethod + fun setOption(tag: Double, key:String, domain: String, value: ReadableMap, promise: Promise) { + val tileStore = RNMBXTileStoreModule.tileStores[tag.toInt()] + if (tileStore == null) { + promise.reject(REACT_CLASS, "No tile store found for tag") + return + } + + tileStore.setOption(key, TileDataDomain.valueOf(domain.uppercase()), value.getDynamic("value").toValue()); + promise.resolve(null) + } + + override fun getName(): String { + return REACT_CLASS + } + + companion object { + const val REACT_CLASS = "RNMBXTileStoreModule" + + var tileStores = mutableMapOf() + var tileStorePathTags = mutableMapOf() + var lastTag = REACT_CLASS.hashCode() % 1096 + } +} \ No newline at end of file diff --git a/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeRNMBXTileStoreModuleSpec.java b/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeRNMBXTileStoreModuleSpec.java new file mode 100644 index 000000000..cf95bc7f2 --- /dev/null +++ b/android/src/main/old-arch/com/rnmapbox/rnmbx/NativeRNMBXTileStoreModuleSpec.java @@ -0,0 +1,45 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateModuleJavaSpec.js + * + * @nolint + */ + +package com.rnmapbox.rnmbx; + +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReactModuleWithSpec; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.turbomodule.core.interfaces.TurboModule; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public abstract class NativeRNMBXTileStoreModuleSpec extends ReactContextBaseJavaModule implements ReactModuleWithSpec, TurboModule { + public static final String NAME = "RNMBXTileStoreModule"; + + public NativeRNMBXTileStoreModuleSpec(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public @Nonnull String getName() { + return NAME; + } + + @ReactMethod + @DoNotStrip + public abstract void shared(@Nullable String path, Promise promise); + + @ReactMethod + @DoNotStrip + public abstract void setOption(double tag, String key, String domain, ReadableMap value, Promise promise); +} diff --git a/docs/docs.json b/docs/docs.json index b2e1bf91f..1e91b20dc 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -8390,5 +8390,52 @@ } } ] + }, + "tileStore": { + "name": "tileStore", + "fileNameWithExt": "TileStore.ts", + "relPath": "src/modules/offline/TileStore.ts", + "description": "TileStore manages downloads and storage for requests to tile-related API endpoints,\nenforcing a disk usage quota: tiles available on disk may be deleted to make room for a new download.\nThis interface can be used by an app developer to set the disk quota.", + "props": [], + "styles": [], + "methods": [ + { + "name": "setOption", + "description": "Sets additional options for this instance that are specific to a data type.\nParams:\nkey – The configuration option that should be changed. Valid keys are listed in \\c TileStoreOptions. domain – The data type this setting should be applied for. value – The value for the configuration option, or null if it should be reset.", + "params": [ + { + "name": "key", + "description": "", + "type": { + "name": "string" + }, + "optional": false + }, + { + "name": "domain", + "description": "", + "type": { + "name": "TileDataDomain" + }, + "optional": false + }, + { + "name": "value", + "description": "", + "type": { + "name": "TileDataValue" + }, + "optional": false + } + ], + "examples": [], + "returns": { + "description": "", + "type": { + "name": "Promise" + } + } + } + ] } } \ No newline at end of file diff --git a/docs/tileStore.md b/docs/tileStore.md new file mode 100644 index 000000000..a7cafcbc4 --- /dev/null +++ b/docs/tileStore.md @@ -0,0 +1,30 @@ + + + + +```tsx +import { tileStore } from '@rnmapbox/maps'; + +tileStore + +``` +TileStore manages downloads and storage for requests to tile-related API endpoints, +enforcing a disk usage quota: tiles available on disk may be deleted to make room for a new download. +This interface can be used by an app developer to set the disk quota. + + + +## methods +### setOption(key, domain, value) + +Sets additional options for this instance that are specific to a data type.
Params:
key – The configuration option that should be changed. Valid keys are listed in \c TileStoreOptions. domain – The data type this setting should be applied for. value – The value for the configuration option, or null if it should be reset. + +#### arguments +| Name | Type | Required | Description | +| ---- | :--: | :------: | :----------: | +| `key` | `string` | `Yes` | | +| `domain` | `TileDataDomain` | `Yes` | | +| `value` | `TileDataValue` | `Yes` | | + + + diff --git a/ios/RNMBX/RNMBXOfflineModule.m b/ios/RNMBX/Offline/RNMBXOfflineModule.m similarity index 100% rename from ios/RNMBX/RNMBXOfflineModule.m rename to ios/RNMBX/Offline/RNMBXOfflineModule.m diff --git a/ios/RNMBX/RNMBXOfflineModule.swift b/ios/RNMBX/Offline/RNMBXOfflineModule.swift similarity index 99% rename from ios/RNMBX/RNMBXOfflineModule.swift rename to ios/RNMBX/Offline/RNMBXOfflineModule.swift index 4364b46af..e305343a7 100644 --- a/ios/RNMBX/RNMBXOfflineModule.swift +++ b/ios/RNMBX/Offline/RNMBXOfflineModule.swift @@ -127,32 +127,32 @@ class RNMBXOfflineModule: RCTEventEmitter { ) @objc override - func startObserving() { + public func startObserving() { super.startObserving() hasListeners = true } @objc override - func stopObserving() { + public func stopObserving() { super.stopObserving() hasListeners = false } @objc override - static func requiresMainQueueSetup() -> Bool { + static public func requiresMainQueueSetup() -> Bool { return true } @objc override - func constantsToExport() -> [AnyHashable: Any]! { + public func constantsToExport() -> [AnyHashable: Any]! { return [:] } @objc override - func supportedEvents() -> [String] { + public func supportedEvents() -> [String] { return [Callbacks.error.rawValue, Callbacks.progress.rawValue] } diff --git a/ios/RNMBX/RNMBXOfflineModuleLegacy.m b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m similarity index 100% rename from ios/RNMBX/RNMBXOfflineModuleLegacy.m rename to ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.m diff --git a/ios/RNMBX/RNMBXOfflineModuleLegacy.swift b/ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift similarity index 100% rename from ios/RNMBX/RNMBXOfflineModuleLegacy.swift rename to ios/RNMBX/Offline/RNMBXOfflineModuleLegacy.swift diff --git a/ios/RNMBX/Offline/RNMBXTileStoreModule.m b/ios/RNMBX/Offline/RNMBXTileStoreModule.m new file mode 100644 index 000000000..a98e102a9 --- /dev/null +++ b/ios/RNMBX/Offline/RNMBXTileStoreModule.m @@ -0,0 +1,9 @@ +#import "React/RCTBridgeModule.h" +#import + +@interface RCT_EXTERN_MODULE(RNMBXTileStoreModule, NSObject) + +RCT_EXTERN_METHOD(shared:(NSString *) path resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) +RCT_EXTERN_METHOD(setOption:(nonnull NSNumber *) tag key:(NSString*) key domain:(NSString*) domain value:(NSDictionary*) value resolver:(RCTPromiseResolveBlock) resolve rejecter:(RCTPromiseRejectBlock) reject) + +@end diff --git a/ios/RNMBX/Offline/RNMBXTileStoreModule.swift b/ios/RNMBX/Offline/RNMBXTileStoreModule.swift new file mode 100644 index 000000000..2ddba16cd --- /dev/null +++ b/ios/RNMBX/Offline/RNMBXTileStoreModule.swift @@ -0,0 +1,69 @@ +import Foundation +import MapboxMaps + +typealias Tag = Int + +@objc(RNMBXTileStoreModule) +class RNMBXTileStoreModule: NSObject { + + static var tileStores : [Tag: TileStore] = [:] + static var tileStorePathTags: [String?:Tag] = [:] + + static var lastTag: Tag = ("RNMBXOfflineModule".hashValue % 1096) + + func shared(path: String?) -> TileStore { + if let path = path { + let url = URL(fileURLWithPath: path) + return TileStore.shared(for: url) + } else { + return TileStore.default + } + } + + @objc + public func shared(_ path: String?, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + if let tag = RNMBXTileStoreModule.tileStorePathTags[path] { + resolver(NSNumber(value: tag)) + } else { + let tileStore = shared(path: path) + RNMBXTileStoreModule.lastTag += 1; + let tag = RNMBXTileStoreModule.lastTag + RNMBXTileStoreModule.tileStores[tag] = tileStore + RNMBXTileStoreModule.tileStorePathTags[path] = tag + resolver(NSNumber(value: tag)) + } + } + + private func tileDataDomain(name: String) -> TileDataDomain? { + switch name { + case "Maps": return .maps + case "Navigation": return .navigation + case "Search": return .search + case "ADAS": return .adas + default: + return nil + } + } + + @objc + func setOption(_ tag: NSNumber, key:String, domain: String, value: NSDictionary, resolver: RCTPromiseResolveBlock, rejecter: RCTPromiseRejectBlock) { + guard let tileStore = RNMBXTileStoreModule.tileStores[tag.intValue] else { + rejecter("invalidArgument","No tile store found for tag \(tag)", nil) + return + } + + + guard let domain = tileDataDomain(name: domain) else { + rejecter("invalidArgument","No domain found for \(domain)", nil) + return + } + + tileStore.setOptionForKey(key, domain: domain, value: value.object(forKey: "value")) + resolver(nil) + } + + @objc + public static func requiresMainQueueSetup() -> Bool { + return true + } +} diff --git a/ios/RNMBX/RNMBXMapView.swift b/ios/RNMBX/RNMBXMapView.swift index d45d24b2c..e343b7dc4 100644 --- a/ios/RNMBX/RNMBXMapView.swift +++ b/ios/RNMBX/RNMBXMapView.swift @@ -178,7 +178,11 @@ open class RNMBXMapView: UIView { #if RNMBX_11 _mapView = MapView(frame: self.bounds, mapInitOptions: MapInitOptions()) #else - let resourceOptions = ResourceOptions(accessToken: RNMBXModule.accessToken!) + let accessToken = RNMBXModule.accessToken + if accessToken == nil { + Logger.log(level: .error, message: "No accessToken set, please call Mapbox.setAccessToken(...)") + } + let resourceOptions = ResourceOptions(accessToken: accessToken ?? "") _mapView = MapView(frame: frame, mapInitOptions: MapInitOptions(resourceOptions: resourceOptions)) #endif _mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] diff --git a/scripts/autogenHelpers/DocJSONBuilder.mjs b/scripts/autogenHelpers/DocJSONBuilder.mjs index eb5439044..d7663a188 100644 --- a/scripts/autogenHelpers/DocJSONBuilder.mjs +++ b/scripts/autogenHelpers/DocJSONBuilder.mjs @@ -5,6 +5,9 @@ import * as url from 'url'; import dir from 'node-dir'; import { parse, utils } from 'react-docgen'; + +import { pascelCase } from './globals.mjs'; + const { parseJsDoc } = utils; import JSDocNodeTree from './JSDocNodeTree.js'; diff --git a/scripts/autogenHelpers/JSDocNodeTree.js b/scripts/autogenHelpers/JSDocNodeTree.js index 91d3e9932..e49f9c778 100644 --- a/scripts/autogenHelpers/JSDocNodeTree.js +++ b/scripts/autogenHelpers/JSDocNodeTree.js @@ -120,7 +120,7 @@ class JSDocNodeTree { } _hasArray(node, propName) { - if (!this._root) { + if (!this._root || !node) { return false; } return Array.isArray(node[propName]) && node[propName].length; diff --git a/scripts/autogenerate.mjs b/scripts/autogenerate.mjs index 11ee0ea58..ea55c4166 100644 --- a/scripts/autogenerate.mjs +++ b/scripts/autogenerate.mjs @@ -1,4 +1,3 @@ -import fs from 'fs'; import path from 'path'; import { execSync } from 'child_process'; import * as url from 'url'; diff --git a/scripts/docjsongenerate.mjs b/scripts/docjsongenerate.mjs new file mode 100644 index 000000000..b7e2e383f --- /dev/null +++ b/scripts/docjsongenerate.mjs @@ -0,0 +1,18 @@ +import path from 'path'; +import * as url from 'url'; + +import DocJSONBuilder from './autogenHelpers/DocJSONBuilder.mjs'; +import { getLayers } from './autogenHelpers/generateCodeWithEjs.mjs'; + +const __dirname = url.fileURLToPath(new URL('.', import.meta.url)); + +async function generate() { + const docsRoot = path.join(__dirname, '..', 'docs'); + const docsJsonPath = path.join(docsRoot, 'docs.json'); + + let layers = getLayers(); + const docBuilder = new DocJSONBuilder(layers); + await docBuilder.generate(docsJsonPath); +} + +generate(); diff --git a/setup-jest.js b/setup-jest.js index b3b42063d..fb8f6f90f 100644 --- a/setup-jest.js +++ b/setup-jest.js @@ -185,6 +185,11 @@ NativeModules.RNMBXViewportModule = { getState: jest.fn(), }; +NativeModules.RNMBXTileStoreModule = { + setOptions: jest.fn(), + shared: jest.fn(), +}; + NativeModules.RNMBXMovePointShapeAnimatorModule = { create: jest.fn(), start: jest.fn(), diff --git a/src/Mapbox.ts b/src/Mapbox.ts index 04a910584..df2293187 100644 --- a/src/Mapbox.ts +++ b/src/Mapbox.ts @@ -50,6 +50,7 @@ export { OfflineCreatePackOptions, } from './modules/offline/offlineManager'; export { default as offlineManagerLegacy } from './modules/offline/offlineManagerLegacy'; +export { default as TileStore } from './modules/offline/TileStore'; export { default as snapshotManager, type SnapshotOptions, diff --git a/src/modules/offline/TileStore.ts b/src/modules/offline/TileStore.ts new file mode 100644 index 000000000..0fc9ba2f0 --- /dev/null +++ b/src/modules/offline/TileStore.ts @@ -0,0 +1,47 @@ +import NativeRNMBXTileStoreModule from '../../specs/NativeRNMBXTileStoreModule'; + +type TileDataDomain = 'Maps' | 'Navigation' | 'Search' | 'ADAS'; +type TileDataValue = string | number; + +/** + * TileStore manages downloads and storage for requests to tile-related API endpoints, + * enforcing a disk usage quota: tiles available on disk may be deleted to make room for a new download. + * This interface can be used by an app developer to set the disk quota. + */ + +export default class TileStore { + __nativeTag: number; + /** + * Creates a TileStore instance for the given storage path. The returned instance exists as long as it is retained by the client. + * If the tile store instance already exists for the given path this method will return it without creating a new instance, + * thus making sure that there is only one tile store instance for a path at a time. If the given path is empty, the tile + * store at the default location is returned. On iOS, this storage path is excluded from automatic cloud backup. On Android, + * please exclude the storage path in your Manifest. + * Please refer to the [Android Documentation](https://developer.android.com/guide/topics/data/autobackup.html#IncludingFiles) for detailed information. + * + * @param path The path on disk where tiles and metadata will be stored + */ + static async shared(path?: string): Promise { + const __nativeTag = await NativeRNMBXTileStoreModule.shared(path); + return new TileStore(__nativeTag); + } + + private constructor(__nativeTag: number) { + this.__nativeTag = __nativeTag; + } + + /** + * Sets additional options for this instance that are specific to a data type. +Params: +key – The configuration option that should be changed. Valid keys are listed in \c TileStoreOptions. domain – The data type this setting should be applied for. value – The value for the configuration option, or null if it should be reset. + */ + async setOption( + key: string, + domain: TileDataDomain, + value: TileDataValue, + ): Promise { + await NativeRNMBXTileStoreModule.setOption(this.__nativeTag, key, domain, { + value, + }); + } +} diff --git a/src/specs/NativeRNMBXTileStoreModule.ts b/src/specs/NativeRNMBXTileStoreModule.ts new file mode 100644 index 000000000..91082e468 --- /dev/null +++ b/src/specs/NativeRNMBXTileStoreModule.ts @@ -0,0 +1,27 @@ +import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport'; +import { Int32 } from 'react-native/Libraries/Types/CodegenTypes'; +import { TurboModuleRegistry } from 'react-native'; + +// eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-unused-vars +type ObjectOr = Object; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type StringOr<_T> = string; + +type Domain = 'Maps' | 'Navigation' | 'Search' | 'ADAS'; + +type Tag = Int32; + +type Value = { value: string | number }; + +export interface Spec extends TurboModule { + shared(path?: string): Promise; + setOption( + tag: Tag, + key: string, + domain: StringOr, + value: ObjectOr, + ): Promise; +} + +export default TurboModuleRegistry.getEnforcing('RNMBXTileStoreModule');