From e20435e6543526c7ea691f5af567bf389d604211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Sun, 25 Jan 2026 21:07:54 +0100 Subject: [PATCH 01/28] basic objc wrapper to use kmp on ios --- Package.swift | 15 +++++++- ios/graphics/Texture/TextureHolder.swift | 11 +++++- ios/maps/MCFontLoader.swift | 1 + ios/maps/MCMapView.swift | 6 +++ ios/maps/MCTextureLoader.swift | 4 +- ios/objc/MCMapCoreObjCFactory.m | 20 ++++++++++ ios/objc/MCMapViewObjC.m | 49 ++++++++++++++++++++++++ ios/objc/include/MCMapCoreObjCFactory.h | 14 +++++++ ios/objc/include/MCMapViewObjC.h | 16 ++++++++ 9 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 ios/objc/MCMapCoreObjCFactory.m create mode 100644 ios/objc/MCMapViewObjC.m create mode 100644 ios/objc/include/MCMapCoreObjCFactory.h create mode 100644 ios/objc/include/MCMapViewObjC.h diff --git a/Package.swift b/Package.swift index 7391a06ec..9a8e3729d 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,10 @@ let package = Package( name: "MapCoreSharedModuleCpp", targets: ["MapCoreSharedModuleCpp"] ), + .library( + name: "MapCoreObjC", + targets: ["MapCoreObjC"] + ), ], dependencies: [ .package(url: "https://github.com/UbiqueInnovation/djinni.git", .upToNextMinor(from: "1.0.9")), @@ -117,7 +121,7 @@ let package = Package( .product(name: "Atomics", package: "swift-atomics"), ], path: "ios", - exclude: ["readme.md"], + exclude: ["readme.md", "objc"], resources: [ .process("graphics/Shader/Metal/") ] @@ -189,6 +193,15 @@ let package = Package( // .disableWarning("reorder"), ] ), + .target( + name: "MapCoreObjC", + dependencies: [ + "MapCore", + "MapCoreSharedModule", + ], + path: "ios/objc", + publicHeadersPath: "include" + ), ], cxxLanguageStandard: .cxx17 ) diff --git a/ios/graphics/Texture/TextureHolder.swift b/ios/graphics/Texture/TextureHolder.swift index 34a2fcd44..f785b812d 100644 --- a/ios/graphics/Texture/TextureHolder.swift +++ b/ios/graphics/Texture/TextureHolder.swift @@ -16,7 +16,7 @@ enum TextureHolderError: Error { case emptyData } -@objc +@objcMembers public class TextureHolder: NSObject, @unchecked Sendable { public let texture: MTLTexture @@ -85,6 +85,15 @@ public class TextureHolder: NSObject, @unchecked Sendable { self.init(texture, textureUsableSize: textureUsableSize) } + @objc(initWithData:) + public convenience init?(data: Data) { + do { + try self.init(data, textureUsableSize: nil) + } catch { + return nil + } + } + public convenience init(_ size: CGSize, drawCallback: (CGContext) -> Void) throws { guard size.width > 0, size.height > 0 else { throw TextureHolderError.emptyData diff --git a/ios/maps/MCFontLoader.swift b/ios/maps/MCFontLoader.swift index 788b5bf70..aa2aa7016 100644 --- a/ios/maps/MCFontLoader.swift +++ b/ios/maps/MCFontLoader.swift @@ -12,6 +12,7 @@ import MapCoreSharedModule import UIKit import os +@objcMembers open class MCFontLoader: NSObject, MCFontLoaderInterface, @unchecked Sendable { // MARK: - Font Atlas Dictionary diff --git a/ios/maps/MCMapView.swift b/ios/maps/MCMapView.swift index e231975e8..d8b31420d 100644 --- a/ios/maps/MCMapView.swift +++ b/ios/maps/MCMapView.swift @@ -13,6 +13,7 @@ import Foundation @preconcurrency import MetalKit import os +@objcMembers open class MCMapView: MTKView { public let mapInterface: MCMapInterface private let renderingContext: RenderingContext @@ -72,6 +73,11 @@ open class MCMapView: MTKView { setup() } + @objc(initWithMapConfig:pixelsPerInch:is3D:) + public convenience init(mapConfig: MCMapConfig, pixelsPerInch: NSNumber?, is3D: Bool) { + self.init(mapConfig: mapConfig, pixelsPerInch: pixelsPerInch?.floatValue, is3D: is3D) + } + @available(*, unavailable) public required init(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") diff --git a/ios/maps/MCTextureLoader.swift b/ios/maps/MCTextureLoader.swift index c4626e708..9a1b3534d 100644 --- a/ios/maps/MCTextureLoader.swift +++ b/ios/maps/MCTextureLoader.swift @@ -16,7 +16,8 @@ import UIKit @available(iOS 14.0, *) private let logger = Logger(subsystem: "maps-core", category: "MCTextureLoader") -open class MCTextureLoader: MCLoaderInterface, @unchecked Sendable { +@objcMembers +open class MCTextureLoader: NSObject, MCLoaderInterface, @unchecked Sendable { public let session: URLSession public var isRasterDebugModeEnabled: Bool @@ -37,6 +38,7 @@ open class MCTextureLoader: MCLoaderInterface, @unchecked Sendable { } isRasterDebugModeEnabled = UserDefaults.standard.bool(forKey: "io.openmobilemaps.debug.rastertiles.enabled") + super.init() } open func loadTexture(_ url: String, etag: String?) -> MCTextureLoaderResult { diff --git a/ios/objc/MCMapCoreObjCFactory.m b/ios/objc/MCMapCoreObjCFactory.m new file mode 100644 index 000000000..e492989b5 --- /dev/null +++ b/ios/objc/MCMapCoreObjCFactory.m @@ -0,0 +1,20 @@ +#import "MCMapCoreObjCFactory.h" + +@import MapCore; +@import MapCoreSharedModule; + +@implementation MCMapCoreObjCFactory + ++ (id)createTextureLoader { + return [[MCTextureLoader alloc] initWithUrlSession:nil]; +} + ++ (id)createFontLoaderWithBundle:(NSBundle *)bundle { + return [[MCFontLoader alloc] initWithBundle:bundle preload:@[]]; +} + ++ (id)createTextureHolderWithData:(NSData *)data { + return [[TextureHolder alloc] initWithData:data]; +} + +@end diff --git a/ios/objc/MCMapViewObjC.m b/ios/objc/MCMapViewObjC.m new file mode 100644 index 000000000..f45e5d5c2 --- /dev/null +++ b/ios/objc/MCMapViewObjC.m @@ -0,0 +1,49 @@ +#import "MCMapViewObjC.h" + +@import MapCore; +@import MapCoreSharedModule; + +#import "MapCore-Swift.h" + +@interface MCMapViewObjC () +@property (nonatomic, strong) MCMapView *mapViewInternal; +@end + +@implementation MCMapViewObjC + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + [self commonInit]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)coder { + if (self = [super initWithCoder:coder]) { + [self commonInit]; + } + return self; +} + +- (UIView *)mapView { + return self.mapViewInternal; +} + +- (MCMapInterface *)mapInterface { + return self.mapViewInternal.mapInterface; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + self.mapViewInternal.frame = self.bounds; +} + +- (void)commonInit { + MCMapConfig *config = [[MCMapConfig alloc] initWithMapCoordinateSystem:[MCCoordinateSystemFactory getEpsg3857System]]; + self.mapViewInternal = [[MCMapView alloc] initWithMapConfig:config pixelsPerInch:nil is3D:NO]; + self.mapViewInternal.frame = self.bounds; + self.mapViewInternal.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self addSubview:self.mapViewInternal]; +} + +@end diff --git a/ios/objc/include/MCMapCoreObjCFactory.h b/ios/objc/include/MCMapCoreObjCFactory.h new file mode 100644 index 000000000..075483a03 --- /dev/null +++ b/ios/objc/include/MCMapCoreObjCFactory.h @@ -0,0 +1,14 @@ +#import +@import MapCoreSharedModule; + +NS_ASSUME_NONNULL_BEGIN + +@interface MCMapCoreObjCFactory : NSObject + ++ (id)createTextureLoader; ++ (id)createFontLoaderWithBundle:(NSBundle *)bundle; ++ (id)createTextureHolderWithData:(NSData *)data; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/objc/include/MCMapViewObjC.h b/ios/objc/include/MCMapViewObjC.h new file mode 100644 index 000000000..3b62a0110 --- /dev/null +++ b/ios/objc/include/MCMapViewObjC.h @@ -0,0 +1,16 @@ +#import +@import MapCoreSharedModule; + +NS_ASSUME_NONNULL_BEGIN + +@interface MCMapViewObjC : UIView + +- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER; + +@property (nonatomic, readonly) UIView *mapView; +@property (nonatomic, readonly) MCMapInterface *mapInterface; + +@end + +NS_ASSUME_NONNULL_END From 3d46afcf6a13ad1ae4f7d02a53a326a6e738d36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 09:07:57 +0100 Subject: [PATCH 02/28] kmp: extract mapcore module --- build.gradle.kts | 266 ++++++++++++++++++ .../kmp/core/feature/map/interop/Coord.kt | 5 + .../feature/map/interop/MapCoreInterop.kt | 7 + ...ProviderLocalDataProviderImplementation.kt | 80 ++++++ .../map/interop/MapFactoryImplementation.kt | 84 ++++++ .../feature/map/interop/MapImplementations.kt | 244 ++++++++++++++++ .../MapTiled2dMapLayerConfigImplementation.kt | 120 ++++++++ .../MapVectorLayerSelectionCallbackProxy.kt | 7 + .../feature/map/interop/MapViewWrapper.kt | 22 ++ .../kmp/core/feature/map/interop/RectCoord.kt | 5 + .../kmp/core/feature/map/interop/Coord.kt | 13 + .../feature/map/interop/MapCameraInterface.kt | 9 + .../feature/map/interop/MapCoreInterop.kt | 5 + .../map/interop/MapDataProviderProtocol.kt | 8 + .../core/feature/map/interop/MapFactory.kt | 24 ++ .../core/feature/map/interop/MapGpsLayer.kt | 11 + .../core/feature/map/interop/MapInterface.kt | 15 + .../feature/map/interop/MapRasterLayer.kt | 3 + .../map/interop/MapTiled2dMapLayerConfig.kt | 26 ++ .../feature/map/interop/MapVectorLayer.kt | 6 + .../map/interop/MapVectorLayerFeatureInfo.kt | 12 + .../MapVectorLayerSelectionCallback.kt | 11 + .../feature/map/interop/MapViewWrapper.kt | 10 + .../kmp/core/feature/map/interop/RectCoord.kt | 9 + .../kmp/core/feature/map/model/GpsMode.kt | 7 + .../kmp/core/feature/map/interop/Coord.kt | 5 + .../kmp/core/feature/map/interop/GpsCoord.kt | 5 + .../MapCameraInterfaceImplementation.kt | 42 +++ .../feature/map/interop/MapCoreInterop.kt | 12 + ...ProviderLocalDataProviderImplementation.kt | 66 +++++ .../map/interop/MapFactoryImplementation.kt | 129 +++++++++ .../map/interop/MapGpsLayerImplementation.kt | 174 ++++++++++++ .../map/interop/MapInterfaceImplementation.kt | 61 ++++ .../interop/MapRasterLayerImplementation.kt | 18 ++ .../MapTiled2dMapLayerConfigImplementation.kt | 109 +++++++ .../interop/MapVectorLayerImplementation.kt | 45 +++ .../MapVectorLayerSelectionCallbackProxy.kt | 56 ++++ .../feature/map/interop/MapViewWrapper.kt | 21 ++ .../kmp/core/feature/map/interop/RectCoord.kt | 5 + .../MapCoreKmp/StartYourBridgeHere.swift | 15 + 40 files changed, 1772 insertions(+) create mode 100644 build.gradle.kts create mode 100644 src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt create mode 100644 src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt create mode 100644 src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt create mode 100644 src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt create mode 100644 src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt create mode 100644 src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt create mode 100644 src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt create mode 100644 src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt create mode 100644 src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderProtocol.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfig.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerFeatureInfo.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallback.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt create mode 100644 src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/model/GpsMode.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/GpsCoord.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt create mode 100644 src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt create mode 100644 src/swift/MapCoreKmp/StartYourBridgeHere.swift diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..a744e905f --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,266 @@ +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.CInteropProcess +import java.net.URI +import javax.inject.Inject + +val mapCoreCheckoutPath = project.layout.projectDirectory.asFile.absolutePath +val mapCoreMetalToolchain = providers.environmentVariable("MAPCORE_METAL_TOOLCHAIN") + .orElse(providers.gradleProperty("mapCoreMetalToolchain")) + .orElse("") +val mapCoreMetallibToolchain = providers.environmentVariable("MAPCORE_METALLIB_TOOLCHAIN") + .orElse(providers.gradleProperty("mapCoreMetallibToolchain")) + .orElse("com.apple.dt.toolchain.XcodeDefault") +val mapCoreMetalTargetSimulator = providers.environmentVariable("MAPCORE_METAL_TARGET_SIMULATOR") + .orElse(providers.gradleProperty("mapCoreMetalTargetSimulator")) + .orElse("air64-apple-ios26.0-simulator") +val mapCoreMetalTargetDevice = providers.environmentVariable("MAPCORE_METAL_TARGET_DEVICE") + .orElse(providers.gradleProperty("mapCoreMetalTargetDevice")) + .orElse("air64-apple-ios26.0") + +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.spmForKmp) +} + +@OptIn(ExperimentalKotlinGradlePluginApi::class) +kotlin { + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") // Opt-in for expect/actual classes + } + + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } + } + + val mapCoreCinteropName = "MapCoreKmp" + val iosTargets = listOf( + iosArm64(), + iosSimulatorArm64() + ) + + iosTargets.forEach { iosTarget -> + if (iosTarget.name == "iosSimulatorArm64") { + iosTarget.compilations { + val main by getting { + cinterops.create(mapCoreCinteropName) + } + } + } + } + + sourceSets { + val commonMain by getting { + dependencies { + api(libs.kotlinx.coroutines) + } + } + val androidMain by getting { + dependencies { + api(libs.openmobilemaps.mapscore) + api(libs.openmobilemaps.layer.gps) + implementation(libs.androidx.lifecycle.viewmodelKtx) + } + } + val iosArm64Main by getting { + languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") + } + val iosSimulatorArm64Main by getting { + languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") + } + } +} + +swiftPackageConfig { + create("MapCoreKmp") { + minIos = "14.0" + bridgeSettings { + cSetting { + headerSearchPath = listOf("Sources/djinni-objc") + } + } + dependency { + localPackage( + mapCoreCheckoutPath, + "maps-core" + ) { + add("MapCoreObjC", exportToKotlin = true) + add("MapCoreSharedModule", exportToKotlin = true) + } + remotePackageVersion( + url = URI("https://github.com/openmobilemaps/layer-gps"), + packageName = "layer-gps", + version = "3.6.0", + ) { + add("LayerGpsSharedModule", exportToKotlin = true) + } + } + } +} + +tasks.withType().configureEach { + if (name.contains("MapCoreKmp")) { + settings.compilerOpts("-I$mapCoreCheckoutPath/external/djinni/support-lib/objc") + settings.compilerOpts("-I$mapCoreCheckoutPath/bridging/ios") + } +} + +val mapCoreSpmBuiltDir = + project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/scratch/arm64 x86_64-apple-ios-simulator/release").get().asFile +mapCoreSpmBuiltDir.mkdirs() + +abstract class CompileMapCoreMetallibTask : DefaultTask() { + @get:Input + abstract val sdk: Property + + @get:Input + abstract val toolchainId: Property + + @get:Input + abstract val metallibToolchainId: Property + + @get:Input + abstract val targetTriple: Property + + @get:InputDirectory + abstract val bundleDir: DirectoryProperty + + @get:InputFiles + abstract val metalSources: ConfigurableFileCollection + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @get:OutputFile + abstract val metallibFile: RegularFileProperty + + @get:Inject + abstract val execOperations: ExecOperations + + @TaskAction + fun run() { + val bundleRoot = bundleDir.get().asFile + if (!bundleRoot.exists()) return + val metalFiles = metalSources.files.sortedBy { it.name } + if (metalFiles.isEmpty()) return + val toolchain = toolchainId.orNull?.takeIf { it.isNotBlank() } + val metallibToolchain = metallibToolchainId.orNull?.takeIf { it.isNotBlank() } + + val outputRoot = outputDir.get().asFile + outputRoot.mkdirs() + + val airFiles = metalFiles.map { file -> + File(outputRoot, "${file.nameWithoutExtension}.air") + } + + metalFiles.zip(airFiles).forEach { (metalFile, airFile) -> + execOperations.exec { + val args = buildList { + add("xcrun") + if (toolchain != null) { + add("--toolchain") + add(toolchain) + } + addAll( + listOf( + "-sdk", + sdk.get(), + "metal", + "-target", + targetTriple.get(), + "-c", + metalFile.absolutePath, + "-o", + airFile.absolutePath, + ), + ) + } + commandLine(args) + } + } + + val metallibArgs = buildList { + add("xcrun") + if (metallibToolchain != null) { + add("--toolchain") + add(metallibToolchain) + } + addAll( + listOf( + "-sdk", + sdk.get(), + "metallib", + ), + ) + airFiles.forEach { add(it.absolutePath) } + add("-o") + add(metallibFile.get().asFile.absolutePath) + } + + execOperations.exec { + commandLine(metallibArgs) + } + } +} + +val compileMapCoreMetallibIosSimulator = tasks.register("compileMapCoreMetallibIosSimulator") { + dependsOn("SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackageIosSimulatorArm64") + sdk.set("iphonesimulator") + toolchainId.set(mapCoreMetalToolchain) + metallibToolchainId.set(mapCoreMetallibToolchain) + targetTriple.set(mapCoreMetalTargetSimulator) + bundleDir.set( + project.layout.buildDirectory + .dir("spmKmpPlugin/MapCoreKmp/scratch/arm64-apple-ios-simulator/release/MapCore_MapCore.bundle"), + ) + outputDir.set(project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/metal/iphonesimulator")) + metalSources.from(bundleDir.map { it.asFileTree.matching { include("**/*.metal") } }) + metallibFile.set(bundleDir.map { it.file("default.metallib") }) +} + +val compileMapCoreMetallibIosArm64 = tasks.register("compileMapCoreMetallibIosArm64") { + dependsOn("SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackageIosArm64") + sdk.set("iphoneos") + toolchainId.set(mapCoreMetalToolchain) + metallibToolchainId.set(mapCoreMetallibToolchain) + targetTriple.set(mapCoreMetalTargetDevice) + bundleDir.set( + project.layout.buildDirectory + .dir("spmKmpPlugin/MapCoreKmp/scratch/arm64-apple-ios/release/MapCore_MapCore.bundle"), + ) + outputDir.set(project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/metal/iphoneos")) + metalSources.from(bundleDir.map { it.asFileTree.matching { include("**/*.metal") } }) + metallibFile.set(bundleDir.map { it.file("default.metallib") }) +} + +tasks.matching { it.name == "compileKotlinIosSimulatorArm64" } + .configureEach { dependsOn(compileMapCoreMetallibIosSimulator) } +tasks.matching { it.name == "compileKotlinIosArm64" } + .configureEach { dependsOn(compileMapCoreMetallibIosArm64) } + +android { + namespace = "io.openmobilemaps.mapscore.kmp" + compileSdk = 36 + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + defaultConfig { + minSdk = 31 + } +} diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt new file mode 100644 index 000000000..1854f7712 --- /dev/null +++ b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt @@ -0,0 +1,5 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import io.openmobilemaps.mapscore.shared.map.coordinates.Coord as MapscoreCoord + +actual typealias Coord = MapscoreCoord diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt new file mode 100644 index 000000000..176f8c253 --- /dev/null +++ b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt @@ -0,0 +1,7 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +actual object MapCoreInterop { + actual fun moveToCenter(coord: Coord) { + coord.hashCode() + } +} diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt new file mode 100644 index 000000000..d4861eec5 --- /dev/null +++ b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt @@ -0,0 +1,80 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import android.graphics.BitmapFactory +import com.snapchat.djinni.Future +import com.snapchat.djinni.Promise +import io.openmobilemaps.mapscore.graphics.BitmapTextureHolder +import io.openmobilemaps.mapscore.shared.map.layers.tiled.vector.Tiled2dMapVectorLayerLocalDataProviderInterface +import io.openmobilemaps.mapscore.shared.map.loader.DataLoaderResult +import io.openmobilemaps.mapscore.shared.map.loader.LoaderStatus +import io.openmobilemaps.mapscore.shared.map.loader.TextureLoaderResult +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.nio.ByteBuffer + +internal class MapDataProviderLocalDataProviderImplementation( + private val dataProvider: MapDataProviderProtocol, + private val coroutineScope: CoroutineScope, +) : Tiled2dMapVectorLayerLocalDataProviderInterface() { + + override fun getStyleJson(): String? = dataProvider.getStyleJson() + + override fun loadSpriteAsync(scale: Int): Future { + val promise = Promise() + coroutineScope.launch(Dispatchers.IO) { + val attempt = runCatching { dataProvider.loadSpriteAsync("", "", scale) } + val texture = attempt.getOrNull() + ?.let { bytes -> BitmapFactory.decodeByteArray(bytes, 0, bytes.size) } + ?.let { BitmapTextureHolder(it) } + val result = if (attempt.isFailure) { + TextureLoaderResult(null, null, LoaderStatus.ERROR_NETWORK, null) + } else { + texture?.let { TextureLoaderResult(it, null, LoaderStatus.OK, null) } + ?: TextureLoaderResult(null, null, LoaderStatus.ERROR_NETWORK, null) + } + promise.setValue(result) + } + return promise.future + } + + override fun loadSpriteJsonAsync(scale: Int): Future { + val promise = Promise() + coroutineScope.launch(Dispatchers.IO) { + val attempt = runCatching { dataProvider.loadSpriteJsonAsync("", "", scale) } + val result = if (attempt.isFailure) { + DataLoaderResult(null, null, LoaderStatus.ERROR_NETWORK, null) + } else { + attempt.getOrNull() + ?.let { bytes -> bytes.asDataLoaderResult() } + ?: DataLoaderResult(null, null, LoaderStatus.OK, null) + } + promise.setValue(result) + } + return promise.future + } + + override fun loadGeojson(sourceName: String, url: String): Future { + val promise = Promise() + coroutineScope.launch(Dispatchers.IO) { + val attempt = runCatching { dataProvider.loadGeojson(sourceName, url) } + val result = if (attempt.isFailure) { + DataLoaderResult(null, null, LoaderStatus.ERROR_NETWORK, null) + } else { + attempt.getOrNull() + ?.let { bytes -> bytes.asDataLoaderResult() } + ?: DataLoaderResult(null, null, LoaderStatus.OK, null) + } + promise.setValue(result) + } + return promise.future + } + + private fun ByteArray.asDataLoaderResult(): DataLoaderResult { + val buffer = ByteBuffer.allocateDirect(size).apply { + put(this@asDataLoaderResult) + flip() + } + return DataLoaderResult(buffer, null, LoaderStatus.OK, null) + } +} diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt new file mode 100644 index 000000000..7d3e0c609 --- /dev/null +++ b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt @@ -0,0 +1,84 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import android.content.Context +import androidx.lifecycle.Lifecycle +import io.openmobilemaps.gps.GpsLayer +import io.openmobilemaps.gps.GpsProviderType +import io.openmobilemaps.gps.style.GpsStyleInfoFactory +import io.openmobilemaps.mapscore.map.layers.TiledRasterLayer +import io.openmobilemaps.mapscore.map.loader.DataLoader +import io.openmobilemaps.mapscore.map.loader.FontLoader +import io.openmobilemaps.mapscore.shared.map.layers.tiled.vector.Tiled2dMapVectorLayerInterface as MapscoreVectorLayer +import kotlinx.coroutines.CoroutineScope +import java.io.File + +actual abstract class MapFactory actual constructor( + platformContext: Any?, + coroutineScope: CoroutineScope?, + lifecycle: Any?, +) { + private val context = platformContext as? Context + private val coroutineScope = coroutineScope + private val lifecycle = lifecycle as? Lifecycle + + actual fun _createVectorLayer( + layerName: String, + dataProvider: MapDataProviderProtocol, + ): MapVectorLayer? { + val context = requireNotNull(context) { "MapFactory requires an Android Context" } + val coroutineScope = requireNotNull(coroutineScope) { "MapFactory requires a CoroutineScope" } + val cacheDir = File(context.cacheDir, "vector").apply { mkdirs() } + val provider = MapDataProviderLocalDataProviderImplementation( + dataProvider = dataProvider, + coroutineScope = coroutineScope, + ) + return MapscoreVectorLayer.createExplicitly( + layerName, + null, + false, + arrayListOf(DataLoader(context, cacheDir, 50L * 1024 * 1024)), + FontLoader(context, "map/fonts/", context.resources.displayMetrics.density), + provider, + null, + null, + null, + )?.let { MapVectorLayerImpl(it) } + } + + actual fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? { + val context = requireNotNull(context) { "MapFactory requires an Android Context" } + val cacheDir = File(context.cacheDir, "raster").apply { mkdirs() } + val loader = DataLoader(context, cacheDir, 25L * 1024 * 1024) + return TiledRasterLayer(MapTiled2dMapLayerConfigImplementation(config), arrayListOf(loader)) + .let { MapRasterLayerImpl(it) } + } + + actual fun _createGpsLayer(): MapGpsLayer? { + val context = requireNotNull(context) { "MapFactory requires an Android Context" } + val locationProvider = GpsProviderType.GOOGLE_FUSED.getProvider(context) + val gpsLayer = GpsLayer( + context = context, + style = GpsStyleInfoFactory.createDefaultStyle(context), + initialLocationProvider = locationProvider, + ).apply { + setHeadingEnabled(false) + setFollowInitializeZoom(25_000f) + lifecycle?.let { registerLifecycle(it) } + } + return MapGpsLayerImpl(GpsLayerHandle(gpsLayer, locationProvider)) + } + + actual companion object { + actual fun create( + platformContext: Any?, + coroutineScope: CoroutineScope?, + lifecycle: Any?, + ): MapFactory = MapFactoryImpl(platformContext, coroutineScope, lifecycle) + } +} + +private class MapFactoryImpl( + platformContext: Any?, + coroutineScope: CoroutineScope?, + lifecycle: Any?, +) : MapFactory(platformContext, coroutineScope, lifecycle) diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt new file mode 100644 index 000000000..96f7f8848 --- /dev/null +++ b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt @@ -0,0 +1,244 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import io.openmobilemaps.mapscore.kmp.feature.map.model.GpsMode +import io.openmobilemaps.gps.GpsLayer +import io.openmobilemaps.gps.providers.LocationProviderInterface +import io.openmobilemaps.gps.shared.gps.GpsMode as MapscoreGpsMode +import io.openmobilemaps.mapscore.map.layers.TiledRasterLayer +import io.openmobilemaps.mapscore.map.view.MapView as MapscoreMapView +import io.openmobilemaps.mapscore.shared.map.LayerInterface +import io.openmobilemaps.mapscore.shared.map.layers.tiled.vector.Tiled2dMapVectorLayerInterface as MapscoreVectorLayer +import io.openmobilemaps.mapscore.shared.map.layers.tiled.vector.Tiled2dMapVectorLayerSelectionCallbackInterface as MapscoreSelectionCallback +import io.openmobilemaps.mapscore.shared.map.layers.tiled.vector.VectorLayerFeatureInfo as MapscoreFeatureInfo +import io.openmobilemaps.mapscore.shared.map.layers.tiled.vector.VectorLayerFeatureInfoValue as MapscoreFeatureInfoValue +import io.openmobilemaps.mapscore.kmp.feature.map.interop.MapVectorLayerFeatureInfo as SharedFeatureInfo +import io.openmobilemaps.mapscore.kmp.feature.map.interop.MapVectorLayerFeatureInfoValue as SharedFeatureInfoValue + +actual abstract class MapInterface actual constructor(nativeHandle: Any?) { + protected val nativeHandle: Any? = nativeHandle + + actual abstract fun _addVectorLayer(layer: MapVectorLayer?) + actual abstract fun _removeVectorLayer(layer: MapVectorLayer?) + actual abstract fun _addRasterLayer(layer: MapRasterLayer?) + actual abstract fun _removeRasterLayer(layer: MapRasterLayer?) + actual abstract fun _addGpsLayer(layer: MapGpsLayer?) + actual abstract fun _removeGpsLayer(layer: MapGpsLayer?) + actual abstract fun _getCamera(): MapCameraInterface? + + actual companion object { + actual fun create(nativeHandle: Any?): MapInterface = MapInterfaceImpl(nativeHandle) + } +} + +private class MapInterfaceImpl(nativeHandle: Any?) : MapInterface(nativeHandle) { + private val mapView = nativeHandle as? MapscoreMapView + private val cameraInterface = MapCameraInterfaceImpl(mapView?.getCamera()) + + override fun _addVectorLayer(layer: MapVectorLayer?) { + val handle = layer as? MapVectorLayerImpl ?: return + handle.layerInterface()?.let { mapView?.addLayer(it) } + } + + override fun _removeVectorLayer(layer: MapVectorLayer?) { + val handle = layer as? MapVectorLayerImpl ?: return + handle.layerInterface()?.let { mapView?.removeLayer(it) } + } + + override fun _addRasterLayer(layer: MapRasterLayer?) { + val handle = layer as? MapRasterLayer ?: return + handle.layerInterface()?.let { mapView?.addLayer(it) } + } + + override fun _removeRasterLayer(layer: MapRasterLayer?) { + val handle = layer as? MapRasterLayer ?: return + handle.layerInterface()?.let { mapView?.removeLayer(it) } + } + + override fun _addGpsLayer(layer: MapGpsLayer?) { + val handle = layer as? MapGpsLayerImpl ?: return + handle.layerInterface()?.let { mapView?.addLayer(it) } + } + + override fun _removeGpsLayer(layer: MapGpsLayer?) { + val handle = layer as? MapGpsLayerImpl ?: return + handle.layerInterface()?.let { mapView?.removeLayer(it) } + } + + override fun _getCamera(): MapCameraInterface? = cameraInterface +} + +actual abstract class MapCameraInterface actual constructor(nativeHandle: Any?) { + protected val nativeHandle: Any? = nativeHandle + + actual abstract fun _setBounds(bounds: RectCoord) + actual abstract fun _moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) + actual abstract fun _setMinZoom(zoom: Double) + actual abstract fun _setMaxZoom(zoom: Double) + actual abstract fun _setBoundsRestrictWholeVisibleRect(enabled: Boolean) +} + +private class MapCameraInterfaceImpl(nativeHandle: Any?) : MapCameraInterface(nativeHandle) { + private val camera = nativeHandle as? io.openmobilemaps.mapscore.map.camera.MapCamera + + override fun _setBounds(bounds: RectCoord) { + camera?.setBounds(bounds) + } + + override fun _moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) { + camera?.moveToCenterPositionZoom(coord, zoom, animated) + } + + override fun _setMinZoom(zoom: Double) { + camera?.setMinZoom(zoom) + } + + override fun _setMaxZoom(zoom: Double) { + camera?.setMaxZoom(zoom) + } + + override fun _setBoundsRestrictWholeVisibleRect(enabled: Boolean) { + camera?.setBoundsRestrictWholeVisibleRect(enabled) + } +} + +actual abstract class MapVectorLayer actual constructor(nativeHandle: Any?) { + protected val nativeHandle: Any? = nativeHandle + + actual abstract fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) + actual abstract fun _setGlobalState(state: Map) +} + +internal class MapVectorLayerImpl(nativeHandle: Any?) : MapVectorLayer(nativeHandle) { + private val layer = nativeHandle as? MapscoreVectorLayer + + override fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) { + val callback = delegate?.let { MapVectorLayerSelectionCallbackAdapterImplementation(it) } + layer?.setSelectionDelegate(callback) + } + + override fun _setGlobalState(state: Map) { + val mapped = HashMap() + state.forEach { (key, value) -> + mapped[key] = value.asMapscore() + } + layer?.setGlobalState(mapped) + } + + internal fun layerInterface(): LayerInterface? = layer?.asLayerInterface() +} + +actual open class MapRasterLayer actual constructor(nativeHandle: Any?) { + protected val nativeHandle: Any? = nativeHandle + + internal fun layerInterface(): LayerInterface? = + (nativeHandle as? TiledRasterLayer)?.layerInterface() +} + +actual abstract class MapGpsLayer actual constructor(nativeHandle: Any?) { + protected val nativeHandle: Any? = nativeHandle + + actual abstract fun _setMode(mode: GpsMode) + actual abstract fun _getMode(): GpsMode + actual abstract fun _setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) + actual abstract fun _notifyPermissionGranted() + actual abstract fun _lastLocation(): Coord? +} + +internal class MapGpsLayerImpl(nativeHandle: Any?) : MapGpsLayer(nativeHandle) { + private val handle = nativeHandle as? GpsLayerHandle + private val gpsLayer = handle?.layer + private val locationProvider = handle?.locationProvider + private var modeListener: ((GpsMode) -> Unit)? = null + + init { + gpsLayer?.setOnModeChangedListener { mode -> + modeListener?.invoke(mode.asShared()) + } + } + + override fun _setMode(mode: GpsMode) { + gpsLayer?.setMode(mode.asMapscore()) + } + + override fun _getMode(): GpsMode = gpsLayer?.layerInterface?.getMode()?.asShared() ?: GpsMode.DISABLED + + override fun _setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) { + modeListener = listener + } + + override fun _notifyPermissionGranted() { + locationProvider?.notifyLocationPermissionGranted() + } + + override fun _lastLocation(): Coord? = locationProvider?.getLastLocation() + + internal fun layerInterface(): LayerInterface? = gpsLayer?.asLayerInterface() +} + +private class MapVectorLayerSelectionCallbackAdapterImplementation( + private val proxy: MapVectorLayerSelectionCallbackProxy, +) : MapscoreSelectionCallback() { + override fun didSelectFeature(featureInfo: MapscoreFeatureInfo, layerIdentifier: String, coord: Coord): Boolean { + val shared = featureInfo.asShared(layerIdentifier) + return proxy.handler._didSelectFeature(shared, coord) + } + + override fun didMultiSelectLayerFeatures( + featureInfos: ArrayList, + layerIdentifier: String, + coord: Coord, + ): Boolean { + return proxy.handler._didMultiSelectLayerFeatures(layerIdentifier, coord) + } + + override fun didClickBackgroundConfirmed(coord: Coord): Boolean { + return proxy.handler._didClickBackgroundConfirmed(coord) + } +} + +private fun MapscoreFeatureInfo.asShared(layerIdentifier: String): SharedFeatureInfo { + val props = properties.mapValues { it.value.asShared() } + return SharedFeatureInfo( + identifier = identifier, + layerIdentifier = layerIdentifier, + properties = props, + ) +} + +private fun MapscoreFeatureInfoValue.asShared(): SharedFeatureInfoValue { + val stringValue = stringVal + ?: intVal?.toString() + ?: doubleVal?.toString() + ?: boolVal?.toString() + val list = listStringVal?.filterIsInstance() + return SharedFeatureInfoValue(stringVal = stringValue, listStringVal = list) +} + +private fun SharedFeatureInfoValue.asMapscore(): MapscoreFeatureInfoValue = + MapscoreFeatureInfoValue( + stringVal, + null, + null, + null, + null, + null, + listStringVal?.let { ArrayList(it) }, + ) + +private fun GpsMode.asMapscore(): MapscoreGpsMode = when (this) { + GpsMode.DISABLED -> MapscoreGpsMode.DISABLED + GpsMode.STANDARD -> MapscoreGpsMode.STANDARD + GpsMode.FOLLOW -> MapscoreGpsMode.FOLLOW +} + +private fun MapscoreGpsMode.asShared(): GpsMode = when (this) { + MapscoreGpsMode.DISABLED -> GpsMode.DISABLED + MapscoreGpsMode.STANDARD -> GpsMode.STANDARD + MapscoreGpsMode.FOLLOW -> GpsMode.FOLLOW + MapscoreGpsMode.FOLLOW_AND_TURN -> GpsMode.FOLLOW +} + +internal data class GpsLayerHandle( + val layer: GpsLayer, + val locationProvider: LocationProviderInterface, +) diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt new file mode 100644 index 000000000..98e73a457 --- /dev/null +++ b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt @@ -0,0 +1,120 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import io.openmobilemaps.mapscore.kmp.feature.map.interop.MapTiled2dMapLayerConfig as SharedLayerConfig +import io.openmobilemaps.mapscore.kmp.feature.map.interop.MapTiled2dMapZoomInfo as SharedZoomInfo +import io.openmobilemaps.mapscore.shared.map.coordinates.Coord as MapscoreCoord +import io.openmobilemaps.mapscore.shared.map.coordinates.CoordinateConversionHelperInterface +import io.openmobilemaps.mapscore.shared.map.layers.tiled.Tiled2dMapLayerConfig as MapscoreLayerConfig +import io.openmobilemaps.mapscore.shared.map.layers.tiled.Tiled2dMapZoomInfo as MapscoreZoomInfo +import io.openmobilemaps.mapscore.shared.map.layers.tiled.Tiled2dMapZoomLevelInfo +import io.openmobilemaps.mapscore.shared.map.layers.tiled.Tiled2dMapVectorSettings +import kotlin.math.pow + +class MapTiled2dMapLayerConfigImplementation( + private val config: SharedLayerConfig, +) : MapscoreLayerConfig() { + + private val coordinateConverter by lazy { + CoordinateConversionHelperInterface.independentInstance() + } + + override fun getCoordinateSystemIdentifier(): Int = config.coordinateSystemIdentifier + + override fun getTileUrl(x: Int, y: Int, t: Int, zoom: Int): String { + val tilesPerAxis = 2.0.pow(zoom.toDouble()) + + val bounds = config.bounds + val wmMinX = bounds.topLeft.x + val wmMaxX = bounds.bottomRight.x + val wmMaxY = bounds.topLeft.y + val wmMinY = bounds.bottomRight.y + + val tileWidth = (wmMaxX - wmMinX) / tilesPerAxis + val tileHeight = (wmMaxY - wmMinY) / tilesPerAxis + + val wmMinTileX = wmMinX + x * tileWidth + val wmMaxTileX = wmMinX + (x + 1) * tileWidth + val wmMaxTileY = wmMaxY - y * tileHeight + val wmMinTileY = wmMaxY - (y + 1) * tileHeight + + val wmTopLeft = MapscoreCoord( + systemIdentifier = config.coordinateSystemIdentifier, + x = wmMinTileX, + y = wmMaxTileY, + z = 0.0, + ) + val wmBottomRight = MapscoreCoord( + systemIdentifier = config.coordinateSystemIdentifier, + x = wmMaxTileX, + y = wmMinTileY, + z = 0.0, + ) + + val lv95TopLeft = coordinateConverter.convert(config.bboxCoordinateSystemIdentifier, wmTopLeft) + val lv95BottomRight = coordinateConverter.convert(config.bboxCoordinateSystemIdentifier, wmBottomRight) + + val bbox = listOf( + lv95TopLeft.x, + lv95BottomRight.y, + lv95BottomRight.x, + lv95TopLeft.y, + ).joinToString(separator = ",") { "%.3f".format(it) } + + val width = config.tileWidth + val height = config.tileHeight + + return config.urlFormat + .replace("{bbox}", bbox, ignoreCase = true) + .replace("{width}", width.toString(), ignoreCase = true) + .replace("{height}", height.toString(), ignoreCase = true) + } + + override fun getZoomLevelInfos(): ArrayList = + ArrayList().apply { + for (zoom in config.minZoomLevel..config.maxZoomLevel) { + add(createZoomLevelInfo(zoom)) + } + } + + override fun getVirtualZoomLevelInfos(): ArrayList = + ArrayList().apply { + if (config.minZoomLevel > 0) { + for (zoom in 0 until config.minZoomLevel) { + add(createZoomLevelInfo(zoom)) + } + } + } + + override fun getZoomInfo(): MapscoreZoomInfo = config.zoomInfo.asMapscore() + + override fun getLayerName(): String = config.layerName + + override fun getVectorSettings(): Tiled2dMapVectorSettings? = null + + override fun getBounds() = config.bounds.asMapscore() + + private fun createZoomLevelInfo(zoomLevel: Int): Tiled2dMapZoomLevelInfo { + val tileCount = 2.0.pow(zoomLevel.toDouble()) + val zoom = config.baseZoom / tileCount + val width = (config.baseWidth / tileCount).toFloat() + return Tiled2dMapZoomLevelInfo( + zoom = zoom, + tileWidthLayerSystemUnits = width, + numTilesX = tileCount.toInt(), + numTilesY = tileCount.toInt(), + numTilesT = 1, + zoomLevelIdentifier = zoomLevel, + bounds = config.bounds.asMapscore(), + ) + } +} + +private fun SharedZoomInfo.asMapscore(): MapscoreZoomInfo = MapscoreZoomInfo( + zoomLevelScaleFactor = zoomLevelScaleFactor.toFloat(), + numDrawPreviousLayers = numDrawPreviousLayers, + numDrawPreviousOrLaterTLayers = numDrawPreviousOrLaterTLayers, + adaptScaleToScreen = adaptScaleToScreen, + maskTile = maskTile, + underzoom = underzoom, + overzoom = overzoom, +) diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt new file mode 100644 index 000000000..3bb6a53e1 --- /dev/null +++ b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt @@ -0,0 +1,7 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +actual class MapVectorLayerSelectionCallbackProxy actual constructor( + handler: MapVectorLayerSelectionCallback, +) { + actual val handler: MapVectorLayerSelectionCallback = handler +} diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt new file mode 100644 index 000000000..e12624fbb --- /dev/null +++ b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt @@ -0,0 +1,22 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import android.view.View +import io.openmobilemaps.mapscore.kmp.feature.map.interop.MapInterface + +actual typealias PlatformMapView = View + +actual class MapViewWrapper actual constructor() { + private lateinit var viewInternal: View + private lateinit var mapInterfaceInternal: MapInterface + + actual val view: View + get() = viewInternal + + actual val mapInterface: MapInterface + get() = mapInterfaceInternal + + constructor(view: View, mapInterface: MapInterface) : this() { + viewInternal = view + mapInterfaceInternal = mapInterface + } +} diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt new file mode 100644 index 000000000..e1bc84bbb --- /dev/null +++ b/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt @@ -0,0 +1,5 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import io.openmobilemaps.mapscore.shared.map.coordinates.RectCoord as MapscoreRectCoord + +actual typealias RectCoord = MapscoreRectCoord diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt new file mode 100644 index 000000000..0cf764b13 --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt @@ -0,0 +1,13 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +expect class Coord( + systemIdentifier: Int, + x: Double, + y: Double, + z: Double, +) { + val systemIdentifier: Int + val x: Double + val y: Double + val z: Double +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt new file mode 100644 index 000000000..90e760d82 --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt @@ -0,0 +1,9 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +expect abstract class MapCameraInterface constructor(nativeHandle: Any? = null) { + abstract fun _setBounds(bounds: RectCoord) + abstract fun _moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) + abstract fun _setMinZoom(zoom: Double) + abstract fun _setMaxZoom(zoom: Double) + abstract fun _setBoundsRestrictWholeVisibleRect(enabled: Boolean) +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt new file mode 100644 index 000000000..eb3724df6 --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt @@ -0,0 +1,5 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +expect object MapCoreInterop { + fun moveToCenter(coord: Coord) +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderProtocol.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderProtocol.kt new file mode 100644 index 000000000..79bad348d --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderProtocol.kt @@ -0,0 +1,8 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +interface MapDataProviderProtocol { + fun getStyleJson(): String? + suspend fun loadGeojson(resourcePath: String, url: String): ByteArray? + suspend fun loadSpriteAsync(resourcePath: String, url: String, scale: Int): ByteArray? + suspend fun loadSpriteJsonAsync(resourcePath: String, url: String, scale: Int): ByteArray? +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt new file mode 100644 index 000000000..39f752be6 --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt @@ -0,0 +1,24 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import kotlinx.coroutines.CoroutineScope + +expect abstract class MapFactory constructor( + platformContext: Any? = null, + coroutineScope: CoroutineScope? = null, + lifecycle: Any? = null, +) { + abstract fun _createVectorLayer( + layerName: String, + dataProvider: MapDataProviderProtocol, + ): MapVectorLayer? + abstract fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? + abstract fun _createGpsLayer(): MapGpsLayer? + + companion object { + fun create( + platformContext: Any? = null, + coroutineScope: CoroutineScope? = null, + lifecycle: Any? = null, + ): MapFactory + } +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt new file mode 100644 index 000000000..b3392f82a --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt @@ -0,0 +1,11 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import io.openmobilemaps.mapscore.kmp.feature.map.model.GpsMode + +expect abstract class MapGpsLayer constructor(nativeHandle: Any? = null) { + abstract fun _setMode(mode: GpsMode) + abstract fun _getMode(): GpsMode + abstract fun _setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) + abstract fun _notifyPermissionGranted() + abstract fun _lastLocation(): Coord? +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt new file mode 100644 index 000000000..7fbfda99d --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt @@ -0,0 +1,15 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +expect abstract class MapInterface constructor(nativeHandle: Any? = null) { + abstract fun _addVectorLayer(layer: MapVectorLayer?) + abstract fun _removeVectorLayer(layer: MapVectorLayer?) + abstract fun _addRasterLayer(layer: MapRasterLayer?) + abstract fun _removeRasterLayer(layer: MapRasterLayer?) + abstract fun _addGpsLayer(layer: MapGpsLayer?) + abstract fun _removeGpsLayer(layer: MapGpsLayer?) + abstract fun _getCamera(): MapCameraInterface? + + companion object { + fun create(nativeHandle: Any?): MapInterface + } +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt new file mode 100644 index 000000000..8f8188186 --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt @@ -0,0 +1,3 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +expect open class MapRasterLayer constructor(nativeHandle: Any? = null) diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfig.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfig.kt new file mode 100644 index 000000000..f6f778579 --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfig.kt @@ -0,0 +1,26 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +data class MapTiled2dMapZoomInfo( + val zoomLevelScaleFactor: Double, + val numDrawPreviousLayers: Int, + val numDrawPreviousOrLaterTLayers: Int, + val adaptScaleToScreen: Boolean, + val maskTile: Boolean, + val underzoom: Boolean, + val overzoom: Boolean, +) + +data class MapTiled2dMapLayerConfig( + val layerName: String, + val urlFormat: String, + val zoomInfo: MapTiled2dMapZoomInfo, + val minZoomLevel: Int, + val maxZoomLevel: Int, + val coordinateSystemIdentifier: Int, + val bboxCoordinateSystemIdentifier: Int, + val bounds: RectCoord, + val baseZoom: Double, + val baseWidth: Double, + val tileWidth: Int = 512, + val tileHeight: Int = 512, +) diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt new file mode 100644 index 000000000..27c71f65a --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt @@ -0,0 +1,6 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +expect abstract class MapVectorLayer constructor(nativeHandle: Any? = null) { + abstract fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) + abstract fun _setGlobalState(state: Map) +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerFeatureInfo.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerFeatureInfo.kt new file mode 100644 index 000000000..180822d32 --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerFeatureInfo.kt @@ -0,0 +1,12 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +data class MapVectorLayerFeatureInfo( + val identifier: String, + val layerIdentifier: String, + val properties: Map, +) + +data class MapVectorLayerFeatureInfoValue( + val stringVal: String? = null, + val listStringVal: List? = null, +) diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallback.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallback.kt new file mode 100644 index 000000000..50a144fc4 --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallback.kt @@ -0,0 +1,11 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +interface MapVectorLayerSelectionCallback { + fun _didSelectFeature(featureInfo: MapVectorLayerFeatureInfo, coord: Coord): Boolean + fun _didMultiSelectLayerFeatures(layerIdentifier: String, coord: Coord): Boolean + fun _didClickBackgroundConfirmed(coord: Coord): Boolean +} + +expect class MapVectorLayerSelectionCallbackProxy(handler: MapVectorLayerSelectionCallback) { + val handler: MapVectorLayerSelectionCallback +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt new file mode 100644 index 000000000..33bfa52fd --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt @@ -0,0 +1,10 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import io.openmobilemaps.mapscore.kmp.feature.map.interop.MapInterface + +expect class PlatformMapView + +expect class MapViewWrapper() { + val view: PlatformMapView + val mapInterface: MapInterface +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt new file mode 100644 index 000000000..ea3365679 --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt @@ -0,0 +1,9 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +expect class RectCoord( + topLeft: Coord, + bottomRight: Coord, +) { + val topLeft: Coord + val bottomRight: Coord +} diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/model/GpsMode.kt b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/model/GpsMode.kt new file mode 100644 index 000000000..53b1c4d28 --- /dev/null +++ b/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/model/GpsMode.kt @@ -0,0 +1,7 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.model + +enum class GpsMode { + DISABLED, + STANDARD, + FOLLOW, +} diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt new file mode 100644 index 000000000..4e299f4c5 --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt @@ -0,0 +1,5 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import MapCoreSharedModule.MCCoord + +actual typealias Coord = MCCoord diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/GpsCoord.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/GpsCoord.kt new file mode 100644 index 000000000..b4fa1fce3 --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/GpsCoord.kt @@ -0,0 +1,5 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import LayerGpsSharedModule.MCCoord + +typealias GpsCoord = MCCoord diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt new file mode 100644 index 000000000..0f2831af8 --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt @@ -0,0 +1,42 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +import MapCoreSharedModule.MCMapCameraInterface + +@OptIn(ExperimentalObjCName::class) +@ObjCName("MapCameraInterface", exact = true) +actual abstract class MapCameraInterface actual constructor(nativeHandle: Any?) { + protected val nativeHandle: Any? = nativeHandle + + actual abstract fun _setBounds(bounds: RectCoord) + actual abstract fun _moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) + actual abstract fun _setMinZoom(zoom: Double) + actual abstract fun _setMaxZoom(zoom: Double) + actual abstract fun _setBoundsRestrictWholeVisibleRect(enabled: Boolean) +} + +internal class MapCameraInterfaceImpl(nativeHandle: Any?) : MapCameraInterface(nativeHandle) { + private val camera = nativeHandle as? MCMapCameraInterface + + override fun _setBounds(bounds: RectCoord) { + camera?.setBounds(bounds) + } + + override fun _moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) { + camera?.moveToCenterPositionZoom(coord, zoom, animated) + } + + override fun _setMinZoom(zoom: Double) { + camera?.setMinZoom(zoom) + } + + override fun _setMaxZoom(zoom: Double) { + camera?.setMaxZoom(zoom) + } + + override fun _setBoundsRestrictWholeVisibleRect(enabled: Boolean) { + camera?.setBoundsRestrictWholeVisibleRect(enabled) + } +} diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt new file mode 100644 index 000000000..e31a8d44a --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt @@ -0,0 +1,12 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +@OptIn(ExperimentalObjCName::class) +@ObjCName("MapCoreInterop", exact = true) +actual object MapCoreInterop { + actual fun moveToCenter(coord: Coord) { + coord.hashCode() + } +} diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt new file mode 100644 index 000000000..46ff95756 --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt @@ -0,0 +1,66 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import MapCoreObjC.MCMapCoreObjCFactory +import MapCoreSharedModule.MCDataLoaderResult +import MapCoreSharedModule.DJFuture +import MapCoreSharedModule.DJPromise +import MapCoreSharedModule.MCLoaderStatusOK +import MapCoreSharedModule.MCTextureLoaderResult +import MapCoreSharedModule.MCTextureHolderInterfaceProtocol +import MapCoreSharedModule.MCTiled2dMapVectorLayerLocalDataProviderInterfaceProtocol +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import platform.Foundation.NSData +import platform.Foundation.create +import platform.darwin.NSObject + +internal class MapDataProviderLocalDataProviderImplementation( + private val dataProvider: MapDataProviderProtocol, +) : NSObject(), MCTiled2dMapVectorLayerLocalDataProviderInterfaceProtocol { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + override fun getStyleJson(): String? = dataProvider.getStyleJson() + + override fun loadGeojson(sourceName: String, url: String): DJFuture { + val promise = DJPromise() + scope.launch { + val result = runCatching { dataProvider.loadGeojson(sourceName, url) }.getOrNull() + val data = result?.toNSData() + promise.setValue(MCDataLoaderResult(data = data, etag = null, status = MCLoaderStatusOK, errorCode = null)) + } + return promise.getFuture() + } + + override fun loadSpriteAsync(spriteId: String, url: String, scale: Int): DJFuture { + val promise = DJPromise() + scope.launch { + val result = runCatching { dataProvider.loadSpriteAsync(spriteId, url, scale) }.getOrNull() + val holder = result?.toNSData() + ?.let { MCMapCoreObjCFactory.createTextureHolderWithData(it) as? MCTextureHolderInterfaceProtocol } + promise.setValue(MCTextureLoaderResult(data = holder, etag = null, status = MCLoaderStatusOK, errorCode = null)) + } + return promise.getFuture() + } + + override fun loadSpriteJsonAsync(spriteId: String, url: String, scale: Int): DJFuture { + val promise = DJPromise() + scope.launch { + val result = runCatching { dataProvider.loadSpriteJsonAsync(spriteId, url, scale) }.getOrNull() + val data = result?.toNSData() + promise.setValue(MCDataLoaderResult(data = data, etag = null, status = MCLoaderStatusOK, errorCode = null)) + } + return promise.getFuture() + } +} + +@OptIn(kotlinx.cinterop.BetaInteropApi::class) +private fun ByteArray.toNSData(): NSData { + if (isEmpty()) return NSData() + return usePinned { pinned -> + NSData.create(bytes = pinned.addressOf(0), length = size.toULong()) + } +} diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt new file mode 100644 index 000000000..e3b1886d7 --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt @@ -0,0 +1,129 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +import MapCoreObjC.MCMapCoreObjCFactory +import MapCoreSharedModule.MCFontLoaderInterfaceProtocol +import MapCoreSharedModule.MCLoaderInterfaceProtocol +import MapCoreSharedModule.MCTiled2dMapRasterLayerInterface +import MapCoreSharedModule.MCTiled2dMapVectorLayerInterface +import platform.Foundation.NSBundle +import platform.Foundation.NSLog +import platform.Foundation.NSNumber + +@OptIn(ExperimentalObjCName::class) +@ObjCName("MapFactory", exact = true) +actual abstract class MapFactory actual constructor( + platformContext: Any?, + coroutineScope: kotlinx.coroutines.CoroutineScope?, + lifecycle: Any?, +) { + protected val platformContext: Any? = platformContext + protected val coroutineScope: kotlinx.coroutines.CoroutineScope? = coroutineScope + protected val lifecycle: Any? = lifecycle + + actual abstract fun _createVectorLayer( + layerName: String, + dataProvider: MapDataProviderProtocol, + ): MapVectorLayer? + actual abstract fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? + actual abstract fun _createGpsLayer(): MapGpsLayer? + + actual companion object { + actual fun create( + platformContext: Any?, + coroutineScope: kotlinx.coroutines.CoroutineScope?, + lifecycle: Any?, + ): MapFactory = MapFactoryImpl(platformContext, coroutineScope, lifecycle) + } + + protected fun findMapBundle(): NSBundle? { + return sharedResourcesBundle() + } + + protected fun logMissing(resource: String) { + NSLog("MapFactory: missing %s", resource) + } + + protected fun sharedResourcesBundle(): NSBundle? { + val path = NSBundle.mainBundle.pathForResource("SharedResources", ofType = "bundle") ?: return null + return NSBundle(path) + } + +} + +private class MapFactoryImpl( + platformContext: Any?, + coroutineScope: kotlinx.coroutines.CoroutineScope?, + lifecycle: Any?, +) : MapFactory(platformContext, coroutineScope, lifecycle) { + override fun _createVectorLayer( + layerName: String, + dataProvider: MapDataProviderProtocol, + ): MapVectorLayer? { + val styleJson = dataProvider.getStyleJson() ?: run { + logMissing("style json for $layerName") + return null + } + val bundle = findMapBundle() ?: run { + logMissing("bundle for MapFonts") + return null + } + val fontLoader = MCMapCoreObjCFactory.createFontLoaderWithBundle(bundle) as? MCFontLoaderInterfaceProtocol + ?: run { + logMissing("font loader") + return null + } + val loader = MCMapCoreObjCFactory.createTextureLoader() as? MCLoaderInterfaceProtocol + ?: run { + logMissing("texture loader") + return null + } + val loaders = listOf(loader) + val provider = MapDataProviderLocalDataProviderImplementation(dataProvider) + val layer = MCTiled2dMapVectorLayerInterface.createExplicitly( + layerName, + styleJson = styleJson, + localStyleJson = NSNumber(bool = true), + loaders = loaders, + fontLoader = fontLoader, + localDataProvider = provider, + customZoomInfo = null, + symbolDelegate = null, + sourceUrlParams = null, + ) + return layer?.let { MapVectorLayerImpl(it) } + } + + override fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? { + val loader = MCMapCoreObjCFactory.createTextureLoader() as? MCLoaderInterfaceProtocol + ?: run { + logMissing("texture loader") + return null + } + val loaders = listOf(loader) + val layer = MCTiled2dMapRasterLayerInterface.create( + MapTiled2dMapLayerConfigImplementation(config), + loaders = loaders, + ) + return layer?.let { MapRasterLayerImpl(it) } + } + + override fun _createGpsLayer(): MapGpsLayer? { + return MapGpsLayerImpl(null) + } +} + +private val fontNames = listOf( + "Frutiger Neue Bold", + "Frutiger Neue Condensed Bold", + "Frutiger Neue Condensed Medium", + "Frutiger Neue Condensed Regular", + "Frutiger Neue Italic", + "Frutiger Neue LT Condensed Bold", + "Frutiger Neue Light", + "Frutiger Neue Medium", + "Frutiger Neue Regular", + "FrutigerLTStd-Roman", +) diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt new file mode 100644 index 000000000..037c4ac98 --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt @@ -0,0 +1,174 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +import LayerGpsSharedModule.MCGpsLayerCallbackInterfaceProtocol +import LayerGpsSharedModule.MCGpsLayerInterface +import LayerGpsSharedModule.MCGpsMode +import LayerGpsSharedModule.MCGpsModeDISABLED +import LayerGpsSharedModule.MCGpsModeFOLLOW +import LayerGpsSharedModule.MCGpsModeFOLLOW_AND_TURN +import LayerGpsSharedModule.MCGpsModeSTANDARD +import LayerGpsSharedModule.MCGpsStyleInfoInterface +import LayerGpsSharedModule.MCColor +import LayerGpsSharedModule.MCTextureHolderInterfaceProtocol as GpsTextureHolderInterfaceProtocol +import MapCoreObjC.MCMapCoreObjCFactory +import MapCoreSharedModule.MCCoordinateSystemIdentifiers +import MapCoreSharedModule.MCLayerInterfaceProtocol as MapCoreLayerInterfaceProtocol +import io.openmobilemaps.mapscore.kmp.feature.map.model.GpsMode +import kotlinx.cinterop.useContents +import platform.CoreLocation.CLHeading +import platform.CoreLocation.CLLocation +import platform.CoreLocation.CLLocationManager +import platform.CoreLocation.CLLocationManagerDelegateProtocol +import platform.darwin.NSObject +import platform.UIKit.UIImage +import platform.UIKit.UIImagePNGRepresentation + +@OptIn(ExperimentalObjCName::class) +@ObjCName("MapGpsLayer", exact = true) +actual abstract class MapGpsLayer actual constructor(nativeHandle: Any?) { + protected val nativeHandle: Any? = nativeHandle + + actual abstract fun _setMode(mode: GpsMode) + actual abstract fun _getMode(): GpsMode + actual abstract fun _setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) + actual abstract fun _notifyPermissionGranted() + actual abstract fun _lastLocation(): Coord? +} + +internal class MapGpsLayerImpl(nativeHandle: Any?) : MapGpsLayer(nativeHandle) { + private val gpsLayer: MCGpsLayerInterface = + requireNotNull(MCGpsLayerInterface.create(styleInfo = defaultStyle())) + private val locationManager = CLLocationManager() + private val locationDelegate = GpsLocationDelegate(this) + private val callbackHandler = GpsLayerCallbackHandler(this) + private var modeListener: ((GpsMode) -> Unit)? = null + private var lastKnownLocation: Coord? = null + + init { + gpsLayer.setCallbackHandler(callbackHandler) + gpsLayer.enableHeading(true) + locationManager.delegate = locationDelegate + locationManager.desiredAccuracy = platform.CoreLocation.kCLLocationAccuracyBest + locationManager.headingFilter = 1.0 + } + + override fun _setMode(mode: GpsMode) { + gpsLayer.setMode(mode.asLayerMode()) + } + + override fun _getMode(): GpsMode = gpsLayer.getMode().asSharedMode() + + override fun _setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) { + modeListener = listener + } + + override fun _notifyPermissionGranted() { + locationManager.startUpdatingLocation() + locationManager.startUpdatingHeading() + } + + override fun _lastLocation(): Coord? = lastKnownLocation + + internal fun layerInterface(): MapCoreLayerInterfaceProtocol? = + gpsLayer.asLayerInterface() as? MapCoreLayerInterfaceProtocol + + internal fun updateLocation(location: CLLocation) { + val coord = location.coordinate.useContents { + GpsCoord( + systemIdentifier = MCCoordinateSystemIdentifiers.EPSG4326(), + x = longitude, + y = latitude, + z = location.altitude, + ) + } + gpsLayer.setDrawPoint(shouldDrawHeading()) + gpsLayer.setDrawHeading(shouldDrawHeading()) + gpsLayer.updatePosition(coord, horizontalAccuracyM = location.horizontalAccuracy) + lastKnownLocation = Coord( + systemIdentifier = coord.systemIdentifier(), + x = coord.x(), + y = coord.y(), + z = coord.z(), + ) + } + + internal fun updateHeading(heading: Double) { + gpsLayer.updateHeading(heading.toFloat()) + } + + internal fun handleModeChanged(mode: MCGpsMode) { + modeListener?.invoke(mode.asSharedMode()) + } + + private fun shouldDrawHeading(): Boolean = true +} + +private class GpsLayerCallbackHandler( + private val layer: MapGpsLayerImpl, +) : NSObject(), MCGpsLayerCallbackInterfaceProtocol { + override fun modeDidChange(mode: MCGpsMode) { + layer.handleModeChanged(mode) + } + + override fun onPointClick(coordinate: GpsCoord) { + // no-op + } +} + +private class GpsLocationDelegate( + private val layer: MapGpsLayerImpl, +) : NSObject(), CLLocationManagerDelegateProtocol { + override fun locationManager(manager: CLLocationManager, didUpdateLocations: List<*>) { + val location = didUpdateLocations.lastOrNull() as? CLLocation ?: return + layer.updateLocation(location) + } + + override fun locationManager(manager: CLLocationManager, didUpdateHeading: CLHeading) { + layer.updateHeading(didUpdateHeading.trueHeading) + } + + override fun locationManager(manager: CLLocationManager, didFailWithError: platform.Foundation.NSError) { + layer._setMode(GpsMode.DISABLED) + } +} + +private fun GpsMode.asLayerMode(): MCGpsMode = when (this) { + GpsMode.DISABLED -> MCGpsModeDISABLED + GpsMode.STANDARD -> MCGpsModeSTANDARD + GpsMode.FOLLOW -> MCGpsModeFOLLOW +} + +private fun MCGpsMode.asSharedMode(): GpsMode = when (this) { + MCGpsModeDISABLED -> GpsMode.DISABLED + MCGpsModeSTANDARD -> GpsMode.STANDARD + MCGpsModeFOLLOW -> GpsMode.FOLLOW + MCGpsModeFOLLOW_AND_TURN -> GpsMode.FOLLOW + else -> GpsMode.STANDARD +} + +private fun defaultStyle(): MCGpsStyleInfoInterface? { + val pointTexture = loadTexture("ic_gps_point") + val headingTexture = loadTexture("ic_gps_direction") + val accuracyColor = MCColor(r = 112f / 255f, g = 173f / 255f, b = 204f / 255f, a = 0.2f) + return MCGpsStyleInfoInterface.create(pointTexture, headingTexture = headingTexture, courseTexture = null, accuracyColor = accuracyColor) +} + +private fun loadTexture(name: String): GpsTextureHolderInterfaceProtocol? { + val bundle = findBundleWithImage(name) ?: return null + val image = UIImage.imageNamed(name, inBundle = bundle, compatibleWithTraitCollection = null) ?: return null + val data = UIImagePNGRepresentation(image) ?: return null + return MCMapCoreObjCFactory.createTextureHolderWithData(data) as? GpsTextureHolderInterfaceProtocol +} + +private fun findBundleWithImage(name: String): platform.Foundation.NSBundle? { + val bundles = (platform.Foundation.NSBundle.allBundles + platform.Foundation.NSBundle.allFrameworks) + .mapNotNull { it as? platform.Foundation.NSBundle } + for (bundle in bundles) { + val image = UIImage.imageNamed(name, inBundle = bundle, compatibleWithTraitCollection = null) + if (image != null) return bundle + } + return null +} diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt new file mode 100644 index 000000000..3bcfa062b --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt @@ -0,0 +1,61 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +import MapCoreSharedModule.MCMapInterface + +@OptIn(ExperimentalObjCName::class) +@ObjCName("MapInterface", exact = true) +actual abstract class MapInterface actual constructor(nativeHandle: Any?) { + protected val nativeHandle: Any? = nativeHandle + + actual abstract fun _addVectorLayer(layer: MapVectorLayer?) + actual abstract fun _removeVectorLayer(layer: MapVectorLayer?) + actual abstract fun _addRasterLayer(layer: MapRasterLayer?) + actual abstract fun _removeRasterLayer(layer: MapRasterLayer?) + actual abstract fun _addGpsLayer(layer: MapGpsLayer?) + actual abstract fun _removeGpsLayer(layer: MapGpsLayer?) + actual abstract fun _getCamera(): MapCameraInterface? + + actual companion object { + actual fun create(nativeHandle: Any?): MapInterface = MapInterfaceImpl(nativeHandle) + } +} + +private class MapInterfaceImpl(nativeHandle: Any?) : MapInterface(nativeHandle) { + private val nativeMapInterface = nativeHandle as? MCMapInterface + private val cameraInterface = MapCameraInterfaceImpl(nativeMapInterface?.getCamera()) + + override fun _addVectorLayer(layer: MapVectorLayer?) { + val handle = layer as? MapVectorLayerImpl ?: return + handle.layerInterface()?.let { nativeMapInterface?.addLayer(it) } + } + + override fun _removeVectorLayer(layer: MapVectorLayer?) { + val handle = layer as? MapVectorLayerImpl ?: return + handle.layerInterface()?.let { nativeMapInterface?.removeLayer(it) } + } + + override fun _addRasterLayer(layer: MapRasterLayer?) { + val handle = layer ?: return + handle.layerInterface()?.let { nativeMapInterface?.addLayer(it) } + } + + override fun _removeRasterLayer(layer: MapRasterLayer?) { + val handle = layer ?: return + handle.layerInterface()?.let { nativeMapInterface?.removeLayer(it) } + } + + override fun _addGpsLayer(layer: MapGpsLayer?) { + val handle = layer as? MapGpsLayerImpl ?: return + handle.layerInterface()?.let { nativeMapInterface?.addLayer(it) } + } + + override fun _removeGpsLayer(layer: MapGpsLayer?) { + val handle = layer as? MapGpsLayerImpl ?: return + handle.layerInterface()?.let { nativeMapInterface?.removeLayer(it) } + } + + override fun _getCamera(): MapCameraInterface? = cameraInterface +} diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt new file mode 100644 index 000000000..d27e0487d --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt @@ -0,0 +1,18 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +import MapCoreSharedModule.MCTiled2dMapRasterLayerInterface + +@OptIn(ExperimentalObjCName::class) +@ObjCName("MapRasterLayer", exact = true) +actual open class MapRasterLayer actual constructor( + nativeHandle: Any?, +) { + private val layer = nativeHandle as? MCTiled2dMapRasterLayerInterface + + internal fun layerInterface() = layer?.asLayerInterface() +} + +internal class MapRasterLayerImpl(nativeHandle: Any?) : MapRasterLayer(nativeHandle) diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt new file mode 100644 index 000000000..5e088172f --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt @@ -0,0 +1,109 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import MapCoreSharedModule.MCCoordinateConversionHelperInterface +import MapCoreSharedModule.MCTiled2dMapLayerConfigProtocol +import MapCoreSharedModule.MCTiled2dMapVectorSettings +import MapCoreSharedModule.MCTiled2dMapZoomInfo +import MapCoreSharedModule.MCTiled2dMapZoomLevelInfo +import kotlin.math.pow +import platform.darwin.NSObject + +internal class MapTiled2dMapLayerConfigImplementation( + private val config: MapTiled2dMapLayerConfig, +) : NSObject(), MCTiled2dMapLayerConfigProtocol { + private val coordinateConverter = MCCoordinateConversionHelperInterface.independentInstance() + + override fun getCoordinateSystemIdentifier(): Int = config.coordinateSystemIdentifier + + override fun getTileUrl(x: Int, y: Int, t: Int, zoom: Int): String { + var url = config.urlFormat + val tilesPerAxis = 2.0.pow(zoom.toDouble()) + + val bounds = config.bounds + val wmMinX = bounds.topLeft.x + val wmMaxX = bounds.bottomRight.x + val wmMaxY = bounds.topLeft.y + val wmMinY = bounds.bottomRight.y + + val totalWidth = wmMaxX - wmMinX + val totalHeight = wmMaxY - wmMinY + + val tileWidth = totalWidth / tilesPerAxis + val tileHeight = totalHeight / tilesPerAxis + + val wmMinTileX = wmMinX + x.toDouble() * tileWidth + val wmMaxTileX = wmMinX + (x + 1.0) * tileWidth + val wmMaxTileY = wmMaxY - y.toDouble() * tileHeight + val wmMinTileY = wmMaxY - (y + 1.0) * tileHeight + + val wmTopLeft = Coord( + systemIdentifier = config.coordinateSystemIdentifier, + x = wmMinTileX, + y = wmMaxTileY, + z = 0.0, + ) + val wmBottomRight = Coord( + systemIdentifier = config.coordinateSystemIdentifier, + x = wmMaxTileX, + y = wmMinTileY, + z = 0.0, + ) + + val targetSystem = config.bboxCoordinateSystemIdentifier + val topLeft = coordinateConverter?.convert(targetSystem, coordinate = wmTopLeft) ?: wmTopLeft + val bottomRight = coordinateConverter?.convert(targetSystem, coordinate = wmBottomRight) ?: wmBottomRight + + val bboxString = "${topLeft.x},${bottomRight.y},${bottomRight.x},${topLeft.y}" + + url = url.replace("{bbox}", bboxString) + url = url.replace("{width}", config.tileWidth.toString()) + url = url.replace("{height}", config.tileHeight.toString()) + url = url.replace("{WIDTH}", config.tileWidth.toString()) + url = url.replace("{HEIGHT}", config.tileHeight.toString()) + return url + } + + override fun getZoomLevelInfos(): List { + return (config.minZoomLevel..config.maxZoomLevel).map { getZoomLevelInfo(it) } + } + + override fun getVirtualZoomLevelInfos(): List { + val minZoom = config.minZoomLevel + if (minZoom <= 0) return emptyList() + return (0 until minZoom).map { getZoomLevelInfo(it) } + } + + override fun getZoomInfo(): MCTiled2dMapZoomInfo { + val info = config.zoomInfo + return MCTiled2dMapZoomInfo( + zoomLevelScaleFactor = info.zoomLevelScaleFactor.toFloat(), + numDrawPreviousLayers = info.numDrawPreviousLayers, + numDrawPreviousOrLaterTLayers = info.numDrawPreviousOrLaterTLayers, + adaptScaleToScreen = info.adaptScaleToScreen, + maskTile = info.maskTile, + underzoom = info.underzoom, + overzoom = info.overzoom, + ) + } + + override fun getLayerName(): String = config.layerName + + override fun getVectorSettings(): MCTiled2dMapVectorSettings? = null + + override fun getBounds(): RectCoord? = config.bounds + + private fun getZoomLevelInfo(zoomLevel: Int): MCTiled2dMapZoomLevelInfo { + val tileCount = 2.0.pow(zoomLevel.toDouble()) + val zoom = config.baseZoom / tileCount + val width = config.baseWidth / tileCount + return MCTiled2dMapZoomLevelInfo( + zoom = zoom, + tileWidthLayerSystemUnits = width.toFloat(), + numTilesX = tileCount.toInt(), + numTilesY = tileCount.toInt(), + numTilesT = 1, + zoomLevelIdentifier = zoomLevel, + bounds = config.bounds, + ) + } +} diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt new file mode 100644 index 000000000..a86446bba --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt @@ -0,0 +1,45 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +import MapCoreSharedModule.MCTiled2dMapVectorLayerInterface +import MapCoreSharedModule.MCVectorLayerFeatureInfoValue + +@OptIn(ExperimentalObjCName::class) +@ObjCName("MapVectorLayer", exact = true) +actual abstract class MapVectorLayer actual constructor(nativeHandle: Any?) { + protected val nativeHandle: Any? = nativeHandle + + actual abstract fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) + actual abstract fun _setGlobalState(state: Map) +} + +internal class MapVectorLayerImpl(nativeHandle: Any?) : MapVectorLayer(nativeHandle) { + private val layer = nativeHandle as? MCTiled2dMapVectorLayerInterface + + override fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) { + layer?.setSelectionDelegate(delegate) + } + + override fun _setGlobalState(state: Map) { + val mapped = mutableMapOf() + state.forEach { (key, value) -> + mapped[key] = value.asMapCore() + } + layer?.setGlobalState(mapped) + } + + internal fun layerInterface() = layer?.asLayerInterface() +} + +private fun MapVectorLayerFeatureInfoValue.asMapCore() = + MCVectorLayerFeatureInfoValue( + stringVal = stringVal, + doubleVal = null, + intVal = null, + boolVal = null, + colorVal = null, + listFloatVal = null, + listStringVal = listStringVal, + ) diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt new file mode 100644 index 000000000..575fbba02 --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt @@ -0,0 +1,56 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +import MapCoreSharedModule.MCVectorLayerFeatureInfo +import MapCoreSharedModule.MCVectorLayerFeatureInfoValue +import MapCoreSharedModule.MCTiled2dMapVectorLayerSelectionCallbackInterfaceProtocol +import platform.darwin.NSObject + +@OptIn(ExperimentalObjCName::class) +@ObjCName("MapVectorLayerSelectionCallbackProxy", exact = true) +actual class MapVectorLayerSelectionCallbackProxy actual constructor( + handler: MapVectorLayerSelectionCallback, +) : NSObject(), MCTiled2dMapVectorLayerSelectionCallbackInterfaceProtocol { + actual val handler: MapVectorLayerSelectionCallback = handler + override fun didSelectFeature(featureInfo: MCVectorLayerFeatureInfo, layerIdentifier: String, coord: Coord): Boolean { + val sharedFeatureInfo = featureInfo.asShared(layerIdentifier) + return handler._didSelectFeature(featureInfo = sharedFeatureInfo, coord = coord) + } + + override fun didMultiSelectLayerFeatures( + featureInfos: List<*>, + layerIdentifier: String, + coord: Coord, + ): Boolean { + return handler._didMultiSelectLayerFeatures(layerIdentifier = layerIdentifier, coord = coord) + } + + override fun didClickBackgroundConfirmed(coord: Coord): Boolean { + return handler._didClickBackgroundConfirmed(coord = coord) + } +} + +private fun MCVectorLayerFeatureInfo.asShared(layerIdentifier: String): MapVectorLayerFeatureInfo { + val props = mutableMapOf() + for (entry in properties.entries) { + val key = entry.key as? String ?: continue + val value = entry.value as? MCVectorLayerFeatureInfoValue ?: continue + props[key] = value.asShared() + } + return MapVectorLayerFeatureInfo( + identifier = identifier, + layerIdentifier = layerIdentifier, + properties = props, + ) +} + +private fun MCVectorLayerFeatureInfoValue.asShared(): MapVectorLayerFeatureInfoValue { + val stringValue = stringVal + ?: intVal?.stringValue + ?: doubleVal?.stringValue + ?: boolVal?.stringValue + val list = listStringVal?.mapNotNull { it as? String } + return MapVectorLayerFeatureInfoValue(stringVal = stringValue, listStringVal = list) +} diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt new file mode 100644 index 000000000..a2b34c227 --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt @@ -0,0 +1,21 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import kotlin.experimental.ExperimentalObjCName +import kotlin.native.ObjCName + +import MapCoreObjC.MCMapViewObjC +import MapCoreSharedModule.MCMapInterface +import platform.UIKit.UIView + +actual typealias PlatformMapView = UIView + +@OptIn(ExperimentalObjCName::class) +@ObjCName("MapViewWrapper", exact = true) +actual class MapViewWrapper actual constructor() { + private val mapView = MCMapViewObjC() + @Suppress("CAST_NEVER_SUCCEEDS") + private val mapInterfaceImplementation = MapInterface.create(mapView.mapInterface as MCMapInterface) + + actual val view: UIView = mapView + actual val mapInterface: MapInterface = mapInterfaceImplementation +} diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt new file mode 100644 index 000000000..5d87abc56 --- /dev/null +++ b/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt @@ -0,0 +1,5 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import MapCoreSharedModule.MCRectCoord + +actual typealias RectCoord = MCRectCoord diff --git a/src/swift/MapCoreKmp/StartYourBridgeHere.swift b/src/swift/MapCoreKmp/StartYourBridgeHere.swift new file mode 100644 index 000000000..057ec9525 --- /dev/null +++ b/src/swift/MapCoreKmp/StartYourBridgeHere.swift @@ -0,0 +1,15 @@ +import Foundation +/** +This is a starting class to set up your bridge. +Ensure that your class is public and has the @objcMembers / @objc annotation. +This file has been created because the folder is empty. +Ignore this file if you don't need it. +**/ + +/** +@objcMembers public class StartHere: NSObject { + public override init() { + super.init() + } +} +**/ From 534004d5bcd6c27747ab324c8427a5aa71f67d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 09:09:11 +0100 Subject: [PATCH 03/28] kmp: set group id --- build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index a744e905f..34c8b8615 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,8 @@ import org.jetbrains.kotlin.gradle.tasks.CInteropProcess import java.net.URI import javax.inject.Inject +group = "io.openmobilemaps.mapscore" + val mapCoreCheckoutPath = project.layout.projectDirectory.asFile.absolutePath val mapCoreMetalToolchain = providers.environmentVariable("MAPCORE_METAL_TOOLCHAIN") .orElse(providers.gradleProperty("mapCoreMetalToolchain")) From 26a8e5e39103e61d289892e2d0e64b1769124962 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 09:24:59 +0100 Subject: [PATCH 04/28] kmp: move sources to kmp directory --- build.gradle.kts | 5 +++++ .../mapscore/kmp/core/feature/map/interop/Coord.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt | 0 .../MapDataProviderLocalDataProviderImplementation.kt | 0 .../kmp/core/feature/map/interop/MapFactoryImplementation.kt | 0 .../kmp/core/feature/map/interop/MapImplementations.kt | 0 .../map/interop/MapTiled2dMapLayerConfigImplementation.kt | 0 .../map/interop/MapVectorLayerSelectionCallbackProxy.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt | 0 .../mapscore/kmp/core/feature/map/interop/RectCoord.kt | 0 .../mapscore/kmp/core/feature/map/interop/Coord.kt | 0 .../kmp/core/feature/map/interop/MapCameraInterface.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt | 0 .../kmp/core/feature/map/interop/MapDataProviderProtocol.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapFactory.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapInterface.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt | 0 .../kmp/core/feature/map/interop/MapTiled2dMapLayerConfig.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt | 0 .../core/feature/map/interop/MapVectorLayerFeatureInfo.kt | 0 .../feature/map/interop/MapVectorLayerSelectionCallback.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt | 0 .../mapscore/kmp/core/feature/map/interop/RectCoord.kt | 0 .../mapscore/kmp/core/feature/map/model/GpsMode.kt | 0 .../mapscore/kmp/core/feature/map/interop/Coord.kt | 0 .../mapscore/kmp/core/feature/map/interop/GpsCoord.kt | 0 .../feature/map/interop/MapCameraInterfaceImplementation.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt | 0 .../MapDataProviderLocalDataProviderImplementation.kt | 0 .../kmp/core/feature/map/interop/MapFactoryImplementation.kt | 0 .../core/feature/map/interop/MapGpsLayerImplementation.kt | 0 .../core/feature/map/interop/MapInterfaceImplementation.kt | 0 .../core/feature/map/interop/MapRasterLayerImplementation.kt | 0 .../map/interop/MapTiled2dMapLayerConfigImplementation.kt | 0 .../core/feature/map/interop/MapVectorLayerImplementation.kt | 0 .../map/interop/MapVectorLayerSelectionCallbackProxy.kt | 0 .../mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt | 0 .../mapscore/kmp/core/feature/map/interop/RectCoord.kt | 0 {src => kmp}/swift/MapCoreKmp/StartYourBridgeHere.swift | 0 40 files changed, 5 insertions(+) rename {src => kmp}/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt (100%) rename {src => kmp}/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt (100%) rename {src => kmp}/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt (100%) rename {src => kmp}/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt (100%) rename {src => kmp}/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt (100%) rename {src => kmp}/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt (100%) rename {src => kmp}/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt (100%) rename {src => kmp}/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt (100%) rename {src => kmp}/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderProtocol.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfig.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerFeatureInfo.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallback.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt (100%) rename {src => kmp}/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/model/GpsMode.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/GpsCoord.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt (100%) rename {src => kmp}/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt (100%) rename {src => kmp}/swift/MapCoreKmp/StartYourBridgeHere.swift (100%) diff --git a/build.gradle.kts b/build.gradle.kts index 34c8b8615..297f76807 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -68,17 +68,22 @@ kotlin { sourceSets { val commonMain by getting { + kotlin.srcDir("kmp/commonMain/kotlin") dependencies { api(libs.kotlinx.coroutines) } } val androidMain by getting { + kotlin.srcDir("kmp/androidMain/kotlin") dependencies { api(libs.openmobilemaps.mapscore) api(libs.openmobilemaps.layer.gps) implementation(libs.androidx.lifecycle.viewmodelKtx) } } + val iosMain by getting { + kotlin.srcDir("kmp/iosMain/kotlin") + } val iosArm64Main by getting { languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") } diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt similarity index 100% rename from src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt rename to kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt similarity index 100% rename from src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt rename to kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt similarity index 100% rename from src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt rename to kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt similarity index 100% rename from src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt rename to kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt similarity index 100% rename from src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt rename to kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt similarity index 100% rename from src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt rename to kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt similarity index 100% rename from src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt rename to kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt similarity index 100% rename from src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt rename to kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt diff --git a/src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt similarity index 100% rename from src/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt rename to kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderProtocol.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderProtocol.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderProtocol.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderProtocol.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfig.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfig.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfig.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfig.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerFeatureInfo.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerFeatureInfo.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerFeatureInfo.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerFeatureInfo.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallback.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallback.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallback.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallback.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt diff --git a/src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/model/GpsMode.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/model/GpsMode.kt similarity index 100% rename from src/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/model/GpsMode.kt rename to kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/model/GpsMode.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/GpsCoord.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/GpsCoord.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/GpsCoord.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/GpsCoord.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCoreInterop.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapDataProviderLocalDataProviderImplementation.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerSelectionCallbackProxy.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapViewWrapper.kt diff --git a/src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt similarity index 100% rename from src/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt rename to kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/RectCoord.kt diff --git a/src/swift/MapCoreKmp/StartYourBridgeHere.swift b/kmp/swift/MapCoreKmp/StartYourBridgeHere.swift similarity index 100% rename from src/swift/MapCoreKmp/StartYourBridgeHere.swift rename to kmp/swift/MapCoreKmp/StartYourBridgeHere.swift From 1ea9d5a848d06623bf79e9b0e7fb9dcb0c40ae2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 09:26:28 +0100 Subject: [PATCH 05/28] kmp: inline plugin and dependency versions --- build.gradle.kts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 297f76807..a34799384 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -33,9 +33,9 @@ val mapCoreMetalTargetDevice = providers.environmentVariable("MAPCORE_METAL_TARG .orElse("air64-apple-ios26.0") plugins { - alias(libs.plugins.kotlin.multiplatform) - alias(libs.plugins.android.library) - alias(libs.plugins.spmForKmp) + id("org.jetbrains.kotlin.multiplatform") version "2.3.0" + id("com.android.library") version "8.12.0" + id("io.github.frankois944.spmForKmp") version "1.4.6" } @OptIn(ExperimentalKotlinGradlePluginApi::class) @@ -70,15 +70,15 @@ kotlin { val commonMain by getting { kotlin.srcDir("kmp/commonMain/kotlin") dependencies { - api(libs.kotlinx.coroutines) + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") } } val androidMain by getting { kotlin.srcDir("kmp/androidMain/kotlin") dependencies { - api(libs.openmobilemaps.mapscore) - api(libs.openmobilemaps.layer.gps) - implementation(libs.androidx.lifecycle.viewmodelKtx) + api("io.openmobilemaps:mapscore:3.6.0") + api("io.openmobilemaps:layer-gps:3.6.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.3") } } val iosMain by getting { From f1fab0413e37357f71e1b9c7b98de49b7e5d3b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 09:29:07 +0100 Subject: [PATCH 06/28] kmp: add settings with plugin repos --- settings.gradle.kts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 settings.gradle.kts diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..f5330b064 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,16 @@ +rootProject.name = "maps-core" + +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + } +} From f278462b3ad4d8f21cd0ee41bc16c593e95180d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 09:39:14 +0100 Subject: [PATCH 07/28] kmp: define iosMain source set --- build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index a34799384..45d0dbe19 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -81,13 +81,15 @@ kotlin { implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.3") } } - val iosMain by getting { + val iosMain by creating { kotlin.srcDir("kmp/iosMain/kotlin") } val iosArm64Main by getting { + dependsOn(iosMain) languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") } val iosSimulatorArm64Main by getting { + dependsOn(iosMain) languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") } } From a616aad26815bee1035b9299b1ed7d1441fc7c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 09:40:21 +0100 Subject: [PATCH 08/28] kmp: wire iosMain and enable AndroidX --- build.gradle.kts | 1 + gradle.properties | 1 + 2 files changed, 2 insertions(+) create mode 100644 gradle.properties diff --git a/build.gradle.kts b/build.gradle.kts index 45d0dbe19..03157e6db 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -82,6 +82,7 @@ kotlin { } } val iosMain by creating { + dependsOn(commonMain) kotlin.srcDir("kmp/iosMain/kotlin") } val iosArm64Main by getting { diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..5bac8ac50 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +android.useAndroidX=true From 79b0230dcf0577301a3ac3cd21c46fb5811f308d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 14:49:35 +0100 Subject: [PATCH 09/28] mapcore: update font loader bundle path --- build.gradle.kts | 2 + ios/maps/MCFontLoader.swift | 29 ++++++++--- ios/objc/MCMapCoreObjCFactory.m | 5 ++ ios/objc/include/MCMapCoreObjCFactory.h | 2 + .../map/interop/MapFactoryImplementation.kt | 51 +++++++++++-------- .../feature/map/interop/MapImplementations.kt | 14 ++--- .../MapTiled2dMapLayerConfigImplementation.kt | 4 +- .../map/interop/MapFactoryImplementation.kt | 3 +- .../map/interop/MapGpsLayerImplementation.kt | 2 +- .../interop/MapRasterLayerImplementation.kt | 2 +- .../MapTiled2dMapLayerConfigImplementation.kt | 2 +- .../interop/MapVectorLayerImplementation.kt | 2 +- kmp/swift/MapCoreKmp/Bundle+Shared.swift | 7 +++ 13 files changed, 81 insertions(+), 44 deletions(-) create mode 100644 kmp/swift/MapCoreKmp/Bundle+Shared.swift diff --git a/build.gradle.kts b/build.gradle.kts index 03157e6db..7c5d3ca82 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -78,7 +78,9 @@ kotlin { dependencies { api("io.openmobilemaps:mapscore:3.6.0") api("io.openmobilemaps:layer-gps:3.6.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.3") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.3") + implementation("ch.ubique.android:djinni-support-lib:1.1.1") } } val iosMain by creating { diff --git a/ios/maps/MCFontLoader.swift b/ios/maps/MCFontLoader.swift index aa2aa7016..7a168b7d2 100644 --- a/ios/maps/MCFontLoader.swift +++ b/ios/maps/MCFontLoader.swift @@ -23,10 +23,18 @@ open class MCFontLoader: NSObject, MCFontLoaderInterface, @unchecked Sendable { // MARK: - Init private let bundle: Bundle + private let resourcePath: String? // the bundle to use for searching for fonts - public init(bundle: Bundle, preload: [String] = []) { + @objc(initWithBundle:preload:) + public convenience init(bundle: Bundle, preload: [String] = []) { + self.init(bundle: bundle, resourcePath: nil, preload: preload) + } + + @objc(initWithBundle:resourcePath:preload:) + public init(bundle: Bundle, resourcePath: String?, preload: [String] = []) { self.bundle = bundle + self.resourcePath = resourcePath pixelsPerInch = if Thread.isMainThread { MainActor.assumeIsolated { @@ -71,7 +79,7 @@ open class MCFontLoader: NSObject, MCFontLoaderInterface, @unchecked Sendable { if let fontData = fontDataDictionary[font.name] { return fontData } - if let path = bundle.path(forResource: font.name, ofType: "json") { + if let path = fontResourcePath(name: font.name, ext: "json") { do { let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe) let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves) @@ -147,16 +155,21 @@ open class MCFontLoader: NSObject, MCFontLoaderInterface, @unchecked Sendable { return fontData } - let image = UIImage(named: font.name, in: bundle, compatibleWith: nil) - - guard let cgImage = image?.cgImage, + guard let path = fontResourcePath(name: font.name, ext: "png"), + let image = UIImage(contentsOfFile: path), + let cgImage = image.cgImage, let textureHolder = try? TextureHolder(cgImage) - else { - return nil - } + else { return nil } fontAtlasDictionary[font.name] = textureHolder return textureHolder } + + private func fontResourcePath(name: String, ext: String) -> String? { + if let resourcePath { + return bundle.path(forResource: name, ofType: ext, inDirectory: resourcePath) + } + return bundle.path(forResource: name, ofType: ext) + } } diff --git a/ios/objc/MCMapCoreObjCFactory.m b/ios/objc/MCMapCoreObjCFactory.m index e492989b5..6d2cb256e 100644 --- a/ios/objc/MCMapCoreObjCFactory.m +++ b/ios/objc/MCMapCoreObjCFactory.m @@ -13,6 +13,11 @@ @implementation MCMapCoreObjCFactory return [[MCFontLoader alloc] initWithBundle:bundle preload:@[]]; } ++ (id)createFontLoaderWithBundle:(NSBundle *)bundle + resourcePath:(NSString *)resourcePath { + return [[MCFontLoader alloc] initWithBundle:bundle resourcePath:resourcePath preload:@[]]; +} + + (id)createTextureHolderWithData:(NSData *)data { return [[TextureHolder alloc] initWithData:data]; } diff --git a/ios/objc/include/MCMapCoreObjCFactory.h b/ios/objc/include/MCMapCoreObjCFactory.h index 075483a03..50cd58531 100644 --- a/ios/objc/include/MCMapCoreObjCFactory.h +++ b/ios/objc/include/MCMapCoreObjCFactory.h @@ -7,6 +7,8 @@ NS_ASSUME_NONNULL_BEGIN + (id)createTextureLoader; + (id)createFontLoaderWithBundle:(NSBundle *)bundle; ++ (id)createFontLoaderWithBundle:(NSBundle *)bundle + resourcePath:(nullable NSString *)resourcePath; + (id)createTextureHolderWithData:(NSData *)data; @end diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt index 7d3e0c609..4b454d13b 100644 --- a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt @@ -17,11 +17,34 @@ actual abstract class MapFactory actual constructor( coroutineScope: CoroutineScope?, lifecycle: Any?, ) { - private val context = platformContext as? Context - private val coroutineScope = coroutineScope - private val lifecycle = lifecycle as? Lifecycle + protected val context = platformContext as? Context + protected val coroutineScope = coroutineScope + protected val lifecycle = lifecycle as? Lifecycle - actual fun _createVectorLayer( + actual abstract fun _createVectorLayer( + layerName: String, + dataProvider: MapDataProviderProtocol, + ): MapVectorLayer? + + actual abstract fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? + + actual abstract fun _createGpsLayer(): MapGpsLayer? + + actual companion object { + actual fun create( + platformContext: Any?, + coroutineScope: CoroutineScope?, + lifecycle: Any?, + ): MapFactory = MapFactoryImpl(platformContext, coroutineScope, lifecycle) + } +} + +private class MapFactoryImpl( + platformContext: Any?, + coroutineScope: CoroutineScope?, + lifecycle: Any?, +) : MapFactory(platformContext, coroutineScope, lifecycle) { + override fun _createVectorLayer( layerName: String, dataProvider: MapDataProviderProtocol, ): MapVectorLayer? { @@ -42,10 +65,10 @@ actual abstract class MapFactory actual constructor( null, null, null, - )?.let { MapVectorLayerImpl(it) } + ).let { MapVectorLayerImpl(it) } } - actual fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? { + override fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? { val context = requireNotNull(context) { "MapFactory requires an Android Context" } val cacheDir = File(context.cacheDir, "raster").apply { mkdirs() } val loader = DataLoader(context, cacheDir, 25L * 1024 * 1024) @@ -53,7 +76,7 @@ actual abstract class MapFactory actual constructor( .let { MapRasterLayerImpl(it) } } - actual fun _createGpsLayer(): MapGpsLayer? { + override fun _createGpsLayer(): MapGpsLayer? { val context = requireNotNull(context) { "MapFactory requires an Android Context" } val locationProvider = GpsProviderType.GOOGLE_FUSED.getProvider(context) val gpsLayer = GpsLayer( @@ -67,18 +90,4 @@ actual abstract class MapFactory actual constructor( } return MapGpsLayerImpl(GpsLayerHandle(gpsLayer, locationProvider)) } - - actual companion object { - actual fun create( - platformContext: Any?, - coroutineScope: CoroutineScope?, - lifecycle: Any?, - ): MapFactory = MapFactoryImpl(platformContext, coroutineScope, lifecycle) - } } - -private class MapFactoryImpl( - platformContext: Any?, - coroutineScope: CoroutineScope?, - lifecycle: Any?, -) : MapFactory(platformContext, coroutineScope, lifecycle) diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt index 96f7f8848..c9099e66b 100644 --- a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt @@ -45,13 +45,11 @@ private class MapInterfaceImpl(nativeHandle: Any?) : MapInterface(nativeHandle) } override fun _addRasterLayer(layer: MapRasterLayer?) { - val handle = layer as? MapRasterLayer ?: return - handle.layerInterface()?.let { mapView?.addLayer(it) } + layer?.layerInterface()?.let { mapView?.addLayer(it) } } override fun _removeRasterLayer(layer: MapRasterLayer?) { - val handle = layer as? MapRasterLayer ?: return - handle.layerInterface()?.let { mapView?.removeLayer(it) } + layer?.layerInterface()?.let { mapView?.removeLayer(it) } } override fun _addGpsLayer(layer: MapGpsLayer?) { @@ -78,7 +76,7 @@ actual abstract class MapCameraInterface actual constructor(nativeHandle: Any?) } private class MapCameraInterfaceImpl(nativeHandle: Any?) : MapCameraInterface(nativeHandle) { - private val camera = nativeHandle as? io.openmobilemaps.mapscore.map.camera.MapCamera + private val camera = nativeHandle as? io.openmobilemaps.mapscore.shared.map.MapCameraInterface override fun _setBounds(bounds: RectCoord) { camera?.setBounds(bounds) @@ -108,7 +106,7 @@ actual abstract class MapVectorLayer actual constructor(nativeHandle: Any?) { actual abstract fun _setGlobalState(state: Map) } -internal class MapVectorLayerImpl(nativeHandle: Any?) : MapVectorLayer(nativeHandle) { +class MapVectorLayerImpl(nativeHandle: Any?) : MapVectorLayer(nativeHandle) { private val layer = nativeHandle as? MapscoreVectorLayer override fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) { @@ -134,6 +132,8 @@ actual open class MapRasterLayer actual constructor(nativeHandle: Any?) { (nativeHandle as? TiledRasterLayer)?.layerInterface() } +class MapRasterLayerImpl(nativeHandle: Any?) : MapRasterLayer(nativeHandle) + actual abstract class MapGpsLayer actual constructor(nativeHandle: Any?) { protected val nativeHandle: Any? = nativeHandle @@ -144,7 +144,7 @@ actual abstract class MapGpsLayer actual constructor(nativeHandle: Any?) { actual abstract fun _lastLocation(): Coord? } -internal class MapGpsLayerImpl(nativeHandle: Any?) : MapGpsLayer(nativeHandle) { +class MapGpsLayerImpl(nativeHandle: Any?) : MapGpsLayer(nativeHandle) { private val handle = nativeHandle as? GpsLayerHandle private val gpsLayer = handle?.layer private val locationProvider = handle?.locationProvider diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt index 98e73a457..7676a8aa9 100644 --- a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt @@ -91,7 +91,7 @@ class MapTiled2dMapLayerConfigImplementation( override fun getVectorSettings(): Tiled2dMapVectorSettings? = null - override fun getBounds() = config.bounds.asMapscore() + override fun getBounds() = config.bounds private fun createZoomLevelInfo(zoomLevel: Int): Tiled2dMapZoomLevelInfo { val tileCount = 2.0.pow(zoomLevel.toDouble()) @@ -104,7 +104,7 @@ class MapTiled2dMapLayerConfigImplementation( numTilesY = tileCount.toInt(), numTilesT = 1, zoomLevelIdentifier = zoomLevel, - bounds = config.bounds.asMapscore(), + bounds = config.bounds, ) } } diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt index e3b1886d7..7a0b933a9 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt @@ -47,8 +47,7 @@ actual abstract class MapFactory actual constructor( } protected fun sharedResourcesBundle(): NSBundle? { - val path = NSBundle.mainBundle.pathForResource("SharedResources", ofType = "bundle") ?: return null - return NSBundle(path) + return MapResourceBundleRegistry.bundleProvider?.invoke() } } diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt index 037c4ac98..c5b08235c 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt @@ -38,7 +38,7 @@ actual abstract class MapGpsLayer actual constructor(nativeHandle: Any?) { actual abstract fun _lastLocation(): Coord? } -internal class MapGpsLayerImpl(nativeHandle: Any?) : MapGpsLayer(nativeHandle) { +class MapGpsLayerImpl(nativeHandle: Any?) : MapGpsLayer(nativeHandle) { private val gpsLayer: MCGpsLayerInterface = requireNotNull(MCGpsLayerInterface.create(styleInfo = defaultStyle())) private val locationManager = CLLocationManager() diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt index d27e0487d..f2ce6f9e0 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt @@ -15,4 +15,4 @@ actual open class MapRasterLayer actual constructor( internal fun layerInterface() = layer?.asLayerInterface() } -internal class MapRasterLayerImpl(nativeHandle: Any?) : MapRasterLayer(nativeHandle) +class MapRasterLayerImpl(nativeHandle: Any?) : MapRasterLayer(nativeHandle) diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt index 5e088172f..e866007a6 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapTiled2dMapLayerConfigImplementation.kt @@ -8,7 +8,7 @@ import MapCoreSharedModule.MCTiled2dMapZoomLevelInfo import kotlin.math.pow import platform.darwin.NSObject -internal class MapTiled2dMapLayerConfigImplementation( +class MapTiled2dMapLayerConfigImplementation( private val config: MapTiled2dMapLayerConfig, ) : NSObject(), MCTiled2dMapLayerConfigProtocol { private val coordinateConverter = MCCoordinateConversionHelperInterface.independentInstance() diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt index a86446bba..1c3b1d434 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt @@ -15,7 +15,7 @@ actual abstract class MapVectorLayer actual constructor(nativeHandle: Any?) { actual abstract fun _setGlobalState(state: Map) } -internal class MapVectorLayerImpl(nativeHandle: Any?) : MapVectorLayer(nativeHandle) { +class MapVectorLayerImpl(nativeHandle: Any?) : MapVectorLayer(nativeHandle) { private val layer = nativeHandle as? MCTiled2dMapVectorLayerInterface override fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) { diff --git a/kmp/swift/MapCoreKmp/Bundle+Shared.swift b/kmp/swift/MapCoreKmp/Bundle+Shared.swift new file mode 100644 index 000000000..7c04bf7a7 --- /dev/null +++ b/kmp/swift/MapCoreKmp/Bundle+Shared.swift @@ -0,0 +1,7 @@ +import Foundation + +extension Bundle { + static var shared: Bundle { + Bundle.module + } +} From fff55ff2497e2f999181e9eb05a7151587704f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 14:55:37 +0100 Subject: [PATCH 10/28] mapcore: add resource bundle registry --- .../core/feature/map/interop/MapResourceBundleRegistry.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapResourceBundleRegistry.kt diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapResourceBundleRegistry.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapResourceBundleRegistry.kt new file mode 100644 index 000000000..33e68d396 --- /dev/null +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapResourceBundleRegistry.kt @@ -0,0 +1,7 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import platform.Foundation.NSBundle + +object MapResourceBundleRegistry { + var bundleProvider: (() -> NSBundle?)? = null +} From bf65b313b77ba00cedb64002236f55bc4afc88b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 14:56:08 +0100 Subject: [PATCH 11/28] gradle + ignore --- .gitignore | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43739 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 251 +++++++++++++++++++++++ gradlew.bat | 94 +++++++++ 5 files changed, 353 insertions(+) create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat diff --git a/.gitignore b/.gitignore index 32b70e14f..9407b7681 100644 --- a/.gitignore +++ b/.gitignore @@ -89,3 +89,4 @@ DerivedData/ compile_commands.json cmake-build-test +/local.properties diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..980502d167d3610f88fa03b2f717935189d9fbcf GIT binary patch literal 43739 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 z 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..9b42019c7 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega From 2c69123211b451d4ead7c514958a54ee80a3875d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 16:20:46 +0100 Subject: [PATCH 12/28] kmp: simplify map interop api --- .../map/interop/MapFactoryImplementation.kt | 17 ++-- .../feature/map/interop/MapImplementations.kt | 92 ++++++++----------- .../feature/map/interop/MapRasterLayer.kt | 11 +++ .../feature/map/interop/MapCameraInterface.kt | 12 +-- .../core/feature/map/interop/MapFactory.kt | 6 +- .../core/feature/map/interop/MapGpsLayer.kt | 10 +- .../core/feature/map/interop/MapInterface.kt | 14 +-- .../feature/map/interop/MapVectorLayer.kt | 4 +- .../feature/map/interop/MapCameraInterface.kt | 5 + .../MapCameraInterfaceImplementation.kt | 42 --------- .../map/interop/MapFactoryImplementation.kt | 14 +-- .../map/interop/MapGpsLayerImplementation.kt | 22 ++--- .../map/interop/MapInterfaceImplementation.kt | 29 +++--- .../interop/MapRasterLayerImplementation.kt | 8 +- .../interop/MapVectorLayerImplementation.kt | 8 +- 15 files changed, 128 insertions(+), 166 deletions(-) create mode 100644 kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt create mode 100644 kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt delete mode 100644 kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt index 4b454d13b..35e19be20 100644 --- a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt @@ -21,14 +21,14 @@ actual abstract class MapFactory actual constructor( protected val coroutineScope = coroutineScope protected val lifecycle = lifecycle as? Lifecycle - actual abstract fun _createVectorLayer( + actual abstract fun createVectorLayer( layerName: String, dataProvider: MapDataProviderProtocol, ): MapVectorLayer? - actual abstract fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? + actual abstract fun createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? - actual abstract fun _createGpsLayer(): MapGpsLayer? + actual abstract fun createGpsLayer(): MapGpsLayer? actual companion object { actual fun create( @@ -44,7 +44,7 @@ private class MapFactoryImpl( coroutineScope: CoroutineScope?, lifecycle: Any?, ) : MapFactory(platformContext, coroutineScope, lifecycle) { - override fun _createVectorLayer( + override fun createVectorLayer( layerName: String, dataProvider: MapDataProviderProtocol, ): MapVectorLayer? { @@ -68,15 +68,16 @@ private class MapFactoryImpl( ).let { MapVectorLayerImpl(it) } } - override fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? { + override fun createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? { val context = requireNotNull(context) { "MapFactory requires an Android Context" } val cacheDir = File(context.cacheDir, "raster").apply { mkdirs() } val loader = DataLoader(context, cacheDir, 25L * 1024 * 1024) - return TiledRasterLayer(MapTiled2dMapLayerConfigImplementation(config), arrayListOf(loader)) - .let { MapRasterLayerImpl(it) } + return MapRasterLayer( + TiledRasterLayer(MapTiled2dMapLayerConfigImplementation(config), arrayListOf(loader)), + ) } - override fun _createGpsLayer(): MapGpsLayer? { + override fun createGpsLayer(): MapGpsLayer? { val context = requireNotNull(context) { "MapFactory requires an Android Context" } val locationProvider = GpsProviderType.GOOGLE_FUSED.getProvider(context) val gpsLayer = GpsLayer( diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt index c9099e66b..4e544b506 100644 --- a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt @@ -4,7 +4,6 @@ import io.openmobilemaps.mapscore.kmp.feature.map.model.GpsMode import io.openmobilemaps.gps.GpsLayer import io.openmobilemaps.gps.providers.LocationProviderInterface import io.openmobilemaps.gps.shared.gps.GpsMode as MapscoreGpsMode -import io.openmobilemaps.mapscore.map.layers.TiledRasterLayer import io.openmobilemaps.mapscore.map.view.MapView as MapscoreMapView import io.openmobilemaps.mapscore.shared.map.LayerInterface import io.openmobilemaps.mapscore.shared.map.layers.tiled.vector.Tiled2dMapVectorLayerInterface as MapscoreVectorLayer @@ -17,13 +16,13 @@ import io.openmobilemaps.mapscore.kmp.feature.map.interop.MapVectorLayerFeatureI actual abstract class MapInterface actual constructor(nativeHandle: Any?) { protected val nativeHandle: Any? = nativeHandle - actual abstract fun _addVectorLayer(layer: MapVectorLayer?) - actual abstract fun _removeVectorLayer(layer: MapVectorLayer?) - actual abstract fun _addRasterLayer(layer: MapRasterLayer?) - actual abstract fun _removeRasterLayer(layer: MapRasterLayer?) - actual abstract fun _addGpsLayer(layer: MapGpsLayer?) - actual abstract fun _removeGpsLayer(layer: MapGpsLayer?) - actual abstract fun _getCamera(): MapCameraInterface? + actual abstract fun addVectorLayer(layer: MapVectorLayer?) + actual abstract fun removeVectorLayer(layer: MapVectorLayer?) + actual abstract fun addRasterLayer(layer: MapRasterLayer?) + actual abstract fun removeRasterLayer(layer: MapRasterLayer?) + actual abstract fun addGpsLayer(layer: MapGpsLayer?) + actual abstract fun removeGpsLayer(layer: MapGpsLayer?) + actual abstract fun getCamera(): MapCameraInterface? actual companion object { actual fun create(nativeHandle: Any?): MapInterface = MapInterfaceImpl(nativeHandle) @@ -34,67 +33,65 @@ private class MapInterfaceImpl(nativeHandle: Any?) : MapInterface(nativeHandle) private val mapView = nativeHandle as? MapscoreMapView private val cameraInterface = MapCameraInterfaceImpl(mapView?.getCamera()) - override fun _addVectorLayer(layer: MapVectorLayer?) { + override fun addVectorLayer(layer: MapVectorLayer?) { val handle = layer as? MapVectorLayerImpl ?: return handle.layerInterface()?.let { mapView?.addLayer(it) } } - override fun _removeVectorLayer(layer: MapVectorLayer?) { + override fun removeVectorLayer(layer: MapVectorLayer?) { val handle = layer as? MapVectorLayerImpl ?: return handle.layerInterface()?.let { mapView?.removeLayer(it) } } - override fun _addRasterLayer(layer: MapRasterLayer?) { + override fun addRasterLayer(layer: MapRasterLayer?) { layer?.layerInterface()?.let { mapView?.addLayer(it) } } - override fun _removeRasterLayer(layer: MapRasterLayer?) { + override fun removeRasterLayer(layer: MapRasterLayer?) { layer?.layerInterface()?.let { mapView?.removeLayer(it) } } - override fun _addGpsLayer(layer: MapGpsLayer?) { + override fun addGpsLayer(layer: MapGpsLayer?) { val handle = layer as? MapGpsLayerImpl ?: return handle.layerInterface()?.let { mapView?.addLayer(it) } } - override fun _removeGpsLayer(layer: MapGpsLayer?) { + override fun removeGpsLayer(layer: MapGpsLayer?) { val handle = layer as? MapGpsLayerImpl ?: return handle.layerInterface()?.let { mapView?.removeLayer(it) } } - override fun _getCamera(): MapCameraInterface? = cameraInterface + override fun getCamera(): MapCameraInterface? = cameraInterface } -actual abstract class MapCameraInterface actual constructor(nativeHandle: Any?) { - protected val nativeHandle: Any? = nativeHandle - - actual abstract fun _setBounds(bounds: RectCoord) - actual abstract fun _moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) - actual abstract fun _setMinZoom(zoom: Double) - actual abstract fun _setMaxZoom(zoom: Double) - actual abstract fun _setBoundsRestrictWholeVisibleRect(enabled: Boolean) +actual abstract class MapCameraInterface actual constructor() { + actual abstract fun setBounds(bounds: RectCoord) + actual abstract fun moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) + actual abstract fun setMinZoom(zoom: Double) + actual abstract fun setMaxZoom(zoom: Double) + actual abstract fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) } -private class MapCameraInterfaceImpl(nativeHandle: Any?) : MapCameraInterface(nativeHandle) { +private class MapCameraInterfaceImpl(private val nativeHandle: Any?) : MapCameraInterface() { private val camera = nativeHandle as? io.openmobilemaps.mapscore.shared.map.MapCameraInterface - override fun _setBounds(bounds: RectCoord) { + override fun setBounds(bounds: RectCoord) { camera?.setBounds(bounds) } - override fun _moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) { + override fun moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) { camera?.moveToCenterPositionZoom(coord, zoom, animated) } - override fun _setMinZoom(zoom: Double) { + override fun setMinZoom(zoom: Double) { camera?.setMinZoom(zoom) } - override fun _setMaxZoom(zoom: Double) { + override fun setMaxZoom(zoom: Double) { camera?.setMaxZoom(zoom) } - override fun _setBoundsRestrictWholeVisibleRect(enabled: Boolean) { + override fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) { camera?.setBoundsRestrictWholeVisibleRect(enabled) } } @@ -102,19 +99,19 @@ private class MapCameraInterfaceImpl(nativeHandle: Any?) : MapCameraInterface(na actual abstract class MapVectorLayer actual constructor(nativeHandle: Any?) { protected val nativeHandle: Any? = nativeHandle - actual abstract fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) - actual abstract fun _setGlobalState(state: Map) + actual abstract fun setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) + actual abstract fun setGlobalState(state: Map) } class MapVectorLayerImpl(nativeHandle: Any?) : MapVectorLayer(nativeHandle) { private val layer = nativeHandle as? MapscoreVectorLayer - override fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) { + override fun setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) { val callback = delegate?.let { MapVectorLayerSelectionCallbackAdapterImplementation(it) } layer?.setSelectionDelegate(callback) } - override fun _setGlobalState(state: Map) { + override fun setGlobalState(state: Map) { val mapped = HashMap() state.forEach { (key, value) -> mapped[key] = value.asMapscore() @@ -125,23 +122,14 @@ class MapVectorLayerImpl(nativeHandle: Any?) : MapVectorLayer(nativeHandle) { internal fun layerInterface(): LayerInterface? = layer?.asLayerInterface() } -actual open class MapRasterLayer actual constructor(nativeHandle: Any?) { - protected val nativeHandle: Any? = nativeHandle - - internal fun layerInterface(): LayerInterface? = - (nativeHandle as? TiledRasterLayer)?.layerInterface() -} - -class MapRasterLayerImpl(nativeHandle: Any?) : MapRasterLayer(nativeHandle) - actual abstract class MapGpsLayer actual constructor(nativeHandle: Any?) { protected val nativeHandle: Any? = nativeHandle - actual abstract fun _setMode(mode: GpsMode) - actual abstract fun _getMode(): GpsMode - actual abstract fun _setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) - actual abstract fun _notifyPermissionGranted() - actual abstract fun _lastLocation(): Coord? + actual abstract fun setMode(mode: GpsMode) + actual abstract fun getMode(): GpsMode + actual abstract fun setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) + actual abstract fun notifyPermissionGranted() + actual abstract fun lastLocation(): Coord? } class MapGpsLayerImpl(nativeHandle: Any?) : MapGpsLayer(nativeHandle) { @@ -156,21 +144,21 @@ class MapGpsLayerImpl(nativeHandle: Any?) : MapGpsLayer(nativeHandle) { } } - override fun _setMode(mode: GpsMode) { + override fun setMode(mode: GpsMode) { gpsLayer?.setMode(mode.asMapscore()) } - override fun _getMode(): GpsMode = gpsLayer?.layerInterface?.getMode()?.asShared() ?: GpsMode.DISABLED + override fun getMode(): GpsMode = gpsLayer?.layerInterface?.getMode()?.asShared() ?: GpsMode.DISABLED - override fun _setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) { + override fun setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) { modeListener = listener } - override fun _notifyPermissionGranted() { + override fun notifyPermissionGranted() { locationProvider?.notifyLocationPermissionGranted() } - override fun _lastLocation(): Coord? = locationProvider?.getLastLocation() + override fun lastLocation(): Coord? = locationProvider?.getLastLocation() internal fun layerInterface(): LayerInterface? = gpsLayer?.asLayerInterface() } diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt new file mode 100644 index 000000000..fe049c900 --- /dev/null +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt @@ -0,0 +1,11 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import io.openmobilemaps.mapscore.map.layers.TiledRasterLayer +import io.openmobilemaps.mapscore.shared.map.LayerInterface + +actual open class MapRasterLayer actual constructor(nativeHandle: Any? = null) { + protected val nativeHandle: Any? = nativeHandle + + internal fun layerInterface(): LayerInterface? = + (nativeHandle as? TiledRasterLayer)?.layerInterface() +} diff --git a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt index 90e760d82..6a64da9a0 100644 --- a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt +++ b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt @@ -1,9 +1,9 @@ package io.openmobilemaps.mapscore.kmp.feature.map.interop -expect abstract class MapCameraInterface constructor(nativeHandle: Any? = null) { - abstract fun _setBounds(bounds: RectCoord) - abstract fun _moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) - abstract fun _setMinZoom(zoom: Double) - abstract fun _setMaxZoom(zoom: Double) - abstract fun _setBoundsRestrictWholeVisibleRect(enabled: Boolean) +expect abstract class MapCameraInterface { + abstract fun setBounds(bounds: RectCoord) + abstract fun moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) + abstract fun setMinZoom(zoom: Double) + abstract fun setMaxZoom(zoom: Double) + abstract fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) } diff --git a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt index 39f752be6..abc9a977c 100644 --- a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt +++ b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactory.kt @@ -7,12 +7,12 @@ expect abstract class MapFactory constructor( coroutineScope: CoroutineScope? = null, lifecycle: Any? = null, ) { - abstract fun _createVectorLayer( + abstract fun createVectorLayer( layerName: String, dataProvider: MapDataProviderProtocol, ): MapVectorLayer? - abstract fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? - abstract fun _createGpsLayer(): MapGpsLayer? + abstract fun createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? + abstract fun createGpsLayer(): MapGpsLayer? companion object { fun create( diff --git a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt index b3392f82a..7d70b910d 100644 --- a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt +++ b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayer.kt @@ -3,9 +3,9 @@ package io.openmobilemaps.mapscore.kmp.feature.map.interop import io.openmobilemaps.mapscore.kmp.feature.map.model.GpsMode expect abstract class MapGpsLayer constructor(nativeHandle: Any? = null) { - abstract fun _setMode(mode: GpsMode) - abstract fun _getMode(): GpsMode - abstract fun _setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) - abstract fun _notifyPermissionGranted() - abstract fun _lastLocation(): Coord? + abstract fun setMode(mode: GpsMode) + abstract fun getMode(): GpsMode + abstract fun setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) + abstract fun notifyPermissionGranted() + abstract fun lastLocation(): Coord? } diff --git a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt index 7fbfda99d..8a7a106d0 100644 --- a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt +++ b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterface.kt @@ -1,13 +1,13 @@ package io.openmobilemaps.mapscore.kmp.feature.map.interop expect abstract class MapInterface constructor(nativeHandle: Any? = null) { - abstract fun _addVectorLayer(layer: MapVectorLayer?) - abstract fun _removeVectorLayer(layer: MapVectorLayer?) - abstract fun _addRasterLayer(layer: MapRasterLayer?) - abstract fun _removeRasterLayer(layer: MapRasterLayer?) - abstract fun _addGpsLayer(layer: MapGpsLayer?) - abstract fun _removeGpsLayer(layer: MapGpsLayer?) - abstract fun _getCamera(): MapCameraInterface? + abstract fun addVectorLayer(layer: MapVectorLayer?) + abstract fun removeVectorLayer(layer: MapVectorLayer?) + abstract fun addRasterLayer(layer: MapRasterLayer?) + abstract fun removeRasterLayer(layer: MapRasterLayer?) + abstract fun addGpsLayer(layer: MapGpsLayer?) + abstract fun removeGpsLayer(layer: MapGpsLayer?) + abstract fun getCamera(): MapCameraInterface? companion object { fun create(nativeHandle: Any?): MapInterface diff --git a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt index 27c71f65a..50f7930f9 100644 --- a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt +++ b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayer.kt @@ -1,6 +1,6 @@ package io.openmobilemaps.mapscore.kmp.feature.map.interop expect abstract class MapVectorLayer constructor(nativeHandle: Any? = null) { - abstract fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) - abstract fun _setGlobalState(state: Map) + abstract fun setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) + abstract fun setGlobalState(state: Map) } diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt new file mode 100644 index 000000000..e4d20d588 --- /dev/null +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt @@ -0,0 +1,5 @@ +package io.openmobilemaps.mapscore.kmp.feature.map.interop + +import MapCoreSharedModule.MCMapCameraInterface + +actual typealias MapCameraInterface = MCMapCameraInterface diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt deleted file mode 100644 index 0f2831af8..000000000 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterfaceImplementation.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.openmobilemaps.mapscore.kmp.feature.map.interop - -import kotlin.experimental.ExperimentalObjCName -import kotlin.native.ObjCName - -import MapCoreSharedModule.MCMapCameraInterface - -@OptIn(ExperimentalObjCName::class) -@ObjCName("MapCameraInterface", exact = true) -actual abstract class MapCameraInterface actual constructor(nativeHandle: Any?) { - protected val nativeHandle: Any? = nativeHandle - - actual abstract fun _setBounds(bounds: RectCoord) - actual abstract fun _moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) - actual abstract fun _setMinZoom(zoom: Double) - actual abstract fun _setMaxZoom(zoom: Double) - actual abstract fun _setBoundsRestrictWholeVisibleRect(enabled: Boolean) -} - -internal class MapCameraInterfaceImpl(nativeHandle: Any?) : MapCameraInterface(nativeHandle) { - private val camera = nativeHandle as? MCMapCameraInterface - - override fun _setBounds(bounds: RectCoord) { - camera?.setBounds(bounds) - } - - override fun _moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) { - camera?.moveToCenterPositionZoom(coord, zoom, animated) - } - - override fun _setMinZoom(zoom: Double) { - camera?.setMinZoom(zoom) - } - - override fun _setMaxZoom(zoom: Double) { - camera?.setMaxZoom(zoom) - } - - override fun _setBoundsRestrictWholeVisibleRect(enabled: Boolean) { - camera?.setBoundsRestrictWholeVisibleRect(enabled) - } -} diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt index 7a0b933a9..d612ed07e 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapFactoryImplementation.kt @@ -23,12 +23,12 @@ actual abstract class MapFactory actual constructor( protected val coroutineScope: kotlinx.coroutines.CoroutineScope? = coroutineScope protected val lifecycle: Any? = lifecycle - actual abstract fun _createVectorLayer( + actual abstract fun createVectorLayer( layerName: String, dataProvider: MapDataProviderProtocol, ): MapVectorLayer? - actual abstract fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? - actual abstract fun _createGpsLayer(): MapGpsLayer? + actual abstract fun createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? + actual abstract fun createGpsLayer(): MapGpsLayer? actual companion object { actual fun create( @@ -57,7 +57,7 @@ private class MapFactoryImpl( coroutineScope: kotlinx.coroutines.CoroutineScope?, lifecycle: Any?, ) : MapFactory(platformContext, coroutineScope, lifecycle) { - override fun _createVectorLayer( + override fun createVectorLayer( layerName: String, dataProvider: MapDataProviderProtocol, ): MapVectorLayer? { @@ -95,7 +95,7 @@ private class MapFactoryImpl( return layer?.let { MapVectorLayerImpl(it) } } - override fun _createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? { + override fun createRasterLayer(config: MapTiled2dMapLayerConfig): MapRasterLayer? { val loader = MCMapCoreObjCFactory.createTextureLoader() as? MCLoaderInterfaceProtocol ?: run { logMissing("texture loader") @@ -106,10 +106,10 @@ private class MapFactoryImpl( MapTiled2dMapLayerConfigImplementation(config), loaders = loaders, ) - return layer?.let { MapRasterLayerImpl(it) } + return layer?.let { MapRasterLayer(it) } } - override fun _createGpsLayer(): MapGpsLayer? { + override fun createGpsLayer(): MapGpsLayer? { return MapGpsLayerImpl(null) } } diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt index c5b08235c..e7e9d60a0 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapGpsLayerImplementation.kt @@ -31,11 +31,11 @@ import platform.UIKit.UIImagePNGRepresentation actual abstract class MapGpsLayer actual constructor(nativeHandle: Any?) { protected val nativeHandle: Any? = nativeHandle - actual abstract fun _setMode(mode: GpsMode) - actual abstract fun _getMode(): GpsMode - actual abstract fun _setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) - actual abstract fun _notifyPermissionGranted() - actual abstract fun _lastLocation(): Coord? + actual abstract fun setMode(mode: GpsMode) + actual abstract fun getMode(): GpsMode + actual abstract fun setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) + actual abstract fun notifyPermissionGranted() + actual abstract fun lastLocation(): Coord? } class MapGpsLayerImpl(nativeHandle: Any?) : MapGpsLayer(nativeHandle) { @@ -55,22 +55,22 @@ class MapGpsLayerImpl(nativeHandle: Any?) : MapGpsLayer(nativeHandle) { locationManager.headingFilter = 1.0 } - override fun _setMode(mode: GpsMode) { + override fun setMode(mode: GpsMode) { gpsLayer.setMode(mode.asLayerMode()) } - override fun _getMode(): GpsMode = gpsLayer.getMode().asSharedMode() + override fun getMode(): GpsMode = gpsLayer.getMode().asSharedMode() - override fun _setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) { + override fun setOnModeChangedListener(listener: ((GpsMode) -> Unit)?) { modeListener = listener } - override fun _notifyPermissionGranted() { + override fun notifyPermissionGranted() { locationManager.startUpdatingLocation() locationManager.startUpdatingHeading() } - override fun _lastLocation(): Coord? = lastKnownLocation + override fun lastLocation(): Coord? = lastKnownLocation internal fun layerInterface(): MapCoreLayerInterfaceProtocol? = gpsLayer.asLayerInterface() as? MapCoreLayerInterfaceProtocol @@ -131,7 +131,7 @@ private class GpsLocationDelegate( } override fun locationManager(manager: CLLocationManager, didFailWithError: platform.Foundation.NSError) { - layer._setMode(GpsMode.DISABLED) + layer.setMode(GpsMode.DISABLED) } } diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt index 3bcfa062b..6dbf49aa7 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapInterfaceImplementation.kt @@ -10,13 +10,13 @@ import MapCoreSharedModule.MCMapInterface actual abstract class MapInterface actual constructor(nativeHandle: Any?) { protected val nativeHandle: Any? = nativeHandle - actual abstract fun _addVectorLayer(layer: MapVectorLayer?) - actual abstract fun _removeVectorLayer(layer: MapVectorLayer?) - actual abstract fun _addRasterLayer(layer: MapRasterLayer?) - actual abstract fun _removeRasterLayer(layer: MapRasterLayer?) - actual abstract fun _addGpsLayer(layer: MapGpsLayer?) - actual abstract fun _removeGpsLayer(layer: MapGpsLayer?) - actual abstract fun _getCamera(): MapCameraInterface? + actual abstract fun addVectorLayer(layer: MapVectorLayer?) + actual abstract fun removeVectorLayer(layer: MapVectorLayer?) + actual abstract fun addRasterLayer(layer: MapRasterLayer?) + actual abstract fun removeRasterLayer(layer: MapRasterLayer?) + actual abstract fun addGpsLayer(layer: MapGpsLayer?) + actual abstract fun removeGpsLayer(layer: MapGpsLayer?) + actual abstract fun getCamera(): MapCameraInterface? actual companion object { actual fun create(nativeHandle: Any?): MapInterface = MapInterfaceImpl(nativeHandle) @@ -25,37 +25,36 @@ actual abstract class MapInterface actual constructor(nativeHandle: Any?) { private class MapInterfaceImpl(nativeHandle: Any?) : MapInterface(nativeHandle) { private val nativeMapInterface = nativeHandle as? MCMapInterface - private val cameraInterface = MapCameraInterfaceImpl(nativeMapInterface?.getCamera()) - override fun _addVectorLayer(layer: MapVectorLayer?) { + override fun addVectorLayer(layer: MapVectorLayer?) { val handle = layer as? MapVectorLayerImpl ?: return handle.layerInterface()?.let { nativeMapInterface?.addLayer(it) } } - override fun _removeVectorLayer(layer: MapVectorLayer?) { + override fun removeVectorLayer(layer: MapVectorLayer?) { val handle = layer as? MapVectorLayerImpl ?: return handle.layerInterface()?.let { nativeMapInterface?.removeLayer(it) } } - override fun _addRasterLayer(layer: MapRasterLayer?) { + override fun addRasterLayer(layer: MapRasterLayer?) { val handle = layer ?: return handle.layerInterface()?.let { nativeMapInterface?.addLayer(it) } } - override fun _removeRasterLayer(layer: MapRasterLayer?) { + override fun removeRasterLayer(layer: MapRasterLayer?) { val handle = layer ?: return handle.layerInterface()?.let { nativeMapInterface?.removeLayer(it) } } - override fun _addGpsLayer(layer: MapGpsLayer?) { + override fun addGpsLayer(layer: MapGpsLayer?) { val handle = layer as? MapGpsLayerImpl ?: return handle.layerInterface()?.let { nativeMapInterface?.addLayer(it) } } - override fun _removeGpsLayer(layer: MapGpsLayer?) { + override fun removeGpsLayer(layer: MapGpsLayer?) { val handle = layer as? MapGpsLayerImpl ?: return handle.layerInterface()?.let { nativeMapInterface?.removeLayer(it) } } - override fun _getCamera(): MapCameraInterface? = cameraInterface + override fun getCamera(): MapCameraInterface? = nativeMapInterface?.getCamera() } diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt index f2ce6f9e0..4797ac72d 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt @@ -4,15 +4,15 @@ import kotlin.experimental.ExperimentalObjCName import kotlin.native.ObjCName import MapCoreSharedModule.MCTiled2dMapRasterLayerInterface +import MapCoreSharedModule.MCLayerInterfaceProtocol @OptIn(ExperimentalObjCName::class) @ObjCName("MapRasterLayer", exact = true) actual open class MapRasterLayer actual constructor( - nativeHandle: Any?, + nativeHandle: Any? = null, ) { private val layer = nativeHandle as? MCTiled2dMapRasterLayerInterface - internal fun layerInterface() = layer?.asLayerInterface() + internal fun layerInterface(): MCLayerInterfaceProtocol? = + layer?.asLayerInterface() as? MCLayerInterfaceProtocol } - -class MapRasterLayerImpl(nativeHandle: Any?) : MapRasterLayer(nativeHandle) diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt index 1c3b1d434..d07760f8f 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapVectorLayerImplementation.kt @@ -11,18 +11,18 @@ import MapCoreSharedModule.MCVectorLayerFeatureInfoValue actual abstract class MapVectorLayer actual constructor(nativeHandle: Any?) { protected val nativeHandle: Any? = nativeHandle - actual abstract fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) - actual abstract fun _setGlobalState(state: Map) + actual abstract fun setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) + actual abstract fun setGlobalState(state: Map) } class MapVectorLayerImpl(nativeHandle: Any?) : MapVectorLayer(nativeHandle) { private val layer = nativeHandle as? MCTiled2dMapVectorLayerInterface - override fun _setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) { + override fun setSelectionDelegate(delegate: MapVectorLayerSelectionCallbackProxy?) { layer?.setSelectionDelegate(delegate) } - override fun _setGlobalState(state: Map) { + override fun setGlobalState(state: Map) { val mapped = mutableMapOf() state.forEach { (key, value) -> mapped[key] = value.asMapCore() From e1708f51c047a17393a84bab92d88a6328b9445a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 16:26:10 +0100 Subject: [PATCH 13/28] kmp: align expect/actual for camera and raster --- .../feature/map/interop/MapImplementations.kt | 21 +++++++++++++------ .../feature/map/interop/MapRasterLayer.kt | 2 +- .../feature/map/interop/MapCameraInterface.kt | 12 +++++------ .../interop/MapRasterLayerImplementation.kt | 2 +- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt index 4e544b506..d9fb3a6d5 100644 --- a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt @@ -64,12 +64,21 @@ private class MapInterfaceImpl(nativeHandle: Any?) : MapInterface(nativeHandle) override fun getCamera(): MapCameraInterface? = cameraInterface } -actual abstract class MapCameraInterface actual constructor() { - actual abstract fun setBounds(bounds: RectCoord) - actual abstract fun moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) - actual abstract fun setMinZoom(zoom: Double) - actual abstract fun setMaxZoom(zoom: Double) - actual abstract fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) +actual open class MapCameraInterface actual constructor() { + open fun setBounds(bounds: RectCoord) { + } + + open fun moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) { + } + + open fun setMinZoom(zoom: Double) { + } + + open fun setMaxZoom(zoom: Double) { + } + + open fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) { + } } private class MapCameraInterfaceImpl(private val nativeHandle: Any?) : MapCameraInterface() { diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt index fe049c900..849c1ba01 100644 --- a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayer.kt @@ -3,7 +3,7 @@ package io.openmobilemaps.mapscore.kmp.feature.map.interop import io.openmobilemaps.mapscore.map.layers.TiledRasterLayer import io.openmobilemaps.mapscore.shared.map.LayerInterface -actual open class MapRasterLayer actual constructor(nativeHandle: Any? = null) { +actual open class MapRasterLayer actual constructor(nativeHandle: Any?) { protected val nativeHandle: Any? = nativeHandle internal fun layerInterface(): LayerInterface? = diff --git a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt index 6a64da9a0..e8212cc11 100644 --- a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt +++ b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt @@ -1,9 +1,9 @@ package io.openmobilemaps.mapscore.kmp.feature.map.interop -expect abstract class MapCameraInterface { - abstract fun setBounds(bounds: RectCoord) - abstract fun moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) - abstract fun setMinZoom(zoom: Double) - abstract fun setMaxZoom(zoom: Double) - abstract fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) +expect open class MapCameraInterface { + open fun setBounds(bounds: RectCoord) + open fun moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) + open fun setMinZoom(zoom: Double) + open fun setMaxZoom(zoom: Double) + open fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) } diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt index 4797ac72d..ed6769624 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt @@ -9,7 +9,7 @@ import MapCoreSharedModule.MCLayerInterfaceProtocol @OptIn(ExperimentalObjCName::class) @ObjCName("MapRasterLayer", exact = true) actual open class MapRasterLayer actual constructor( - nativeHandle: Any? = null, + nativeHandle: Any?, ) { private val layer = nativeHandle as? MCTiled2dMapRasterLayerInterface From 1b2814958040f61ea02dbe22f1ef0fa592616ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 16:29:59 +0100 Subject: [PATCH 14/28] kmp: align camera parameter names --- .../feature/map/interop/MapImplementations.kt | 18 +++++++++--------- .../feature/map/interop/MapCameraInterface.kt | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt index d9fb3a6d5..bb7629445 100644 --- a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt @@ -68,13 +68,13 @@ actual open class MapCameraInterface actual constructor() { open fun setBounds(bounds: RectCoord) { } - open fun moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) { + open fun moveToCenterPositionZoom(centerPosition: Coord, zoom: Double, animated: Boolean) { } - open fun setMinZoom(zoom: Double) { + open fun setMinZoom(minZoom: Double) { } - open fun setMaxZoom(zoom: Double) { + open fun setMaxZoom(maxZoom: Double) { } open fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) { @@ -88,16 +88,16 @@ private class MapCameraInterfaceImpl(private val nativeHandle: Any?) : MapCamera camera?.setBounds(bounds) } - override fun moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) { - camera?.moveToCenterPositionZoom(coord, zoom, animated) + override fun moveToCenterPositionZoom(centerPosition: Coord, zoom: Double, animated: Boolean) { + camera?.moveToCenterPositionZoom(centerPosition, zoom, animated) } - override fun setMinZoom(zoom: Double) { - camera?.setMinZoom(zoom) + override fun setMinZoom(minZoom: Double) { + camera?.setMinZoom(minZoom) } - override fun setMaxZoom(zoom: Double) { - camera?.setMaxZoom(zoom) + override fun setMaxZoom(maxZoom: Double) { + camera?.setMaxZoom(maxZoom) } override fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) { diff --git a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt index e8212cc11..e8ee996b7 100644 --- a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt +++ b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt @@ -2,8 +2,8 @@ package io.openmobilemaps.mapscore.kmp.feature.map.interop expect open class MapCameraInterface { open fun setBounds(bounds: RectCoord) - open fun moveToCenterPositionZoom(coord: Coord, zoom: Double, animated: Boolean) - open fun setMinZoom(zoom: Double) - open fun setMaxZoom(zoom: Double) + open fun moveToCenterPositionZoom(centerPosition: Coord, zoom: Double, animated: Boolean) + open fun setMinZoom(minZoom: Double) + open fun setMaxZoom(maxZoom: Double) open fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) } From 06f33ba248fa76522f49c21302caee149e043716 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 16:33:46 +0100 Subject: [PATCH 15/28] kmp: fix camera expect/actual members --- .../kmp/core/feature/map/interop/MapImplementations.kt | 10 +++++----- .../kmp/core/feature/map/interop/MapCameraInterface.kt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt index bb7629445..79197c190 100644 --- a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt @@ -65,19 +65,19 @@ private class MapInterfaceImpl(nativeHandle: Any?) : MapInterface(nativeHandle) } actual open class MapCameraInterface actual constructor() { - open fun setBounds(bounds: RectCoord) { + actual open fun setBounds(bounds: RectCoord) { } - open fun moveToCenterPositionZoom(centerPosition: Coord, zoom: Double, animated: Boolean) { + actual open fun moveToCenterPositionZoom(centerPosition: Coord, zoom: Double, animated: Boolean) { } - open fun setMinZoom(minZoom: Double) { + actual open fun setMinZoom(minZoom: Double) { } - open fun setMaxZoom(maxZoom: Double) { + actual open fun setMaxZoom(maxZoom: Double) { } - open fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) { + actual open fun setBoundsRestrictWholeVisibleRect(enabled: Boolean) { } } diff --git a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt index e8ee996b7..9b647fa9f 100644 --- a/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt +++ b/kmp/commonMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapCameraInterface.kt @@ -1,6 +1,6 @@ package io.openmobilemaps.mapscore.kmp.feature.map.interop -expect open class MapCameraInterface { +expect open class MapCameraInterface() { open fun setBounds(bounds: RectCoord) open fun moveToCenterPositionZoom(centerPosition: Coord, zoom: Double, animated: Boolean) open fun setMinZoom(minZoom: Double) From 1bb478b76a5ab8176f25c5bb007ff8c974e7f41d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 16:38:32 +0100 Subject: [PATCH 16/28] kmp: remove redundant layer cast --- .../core/feature/map/interop/MapRasterLayerImplementation.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt index ed6769624..566061987 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapRasterLayerImplementation.kt @@ -14,5 +14,5 @@ actual open class MapRasterLayer actual constructor( private val layer = nativeHandle as? MCTiled2dMapRasterLayerInterface internal fun layerInterface(): MCLayerInterfaceProtocol? = - layer?.asLayerInterface() as? MCLayerInterfaceProtocol + layer?.asLayerInterface() } From 573ad1265e9b159dffa6ff30fdd908cbe975ba47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 16:45:07 +0100 Subject: [PATCH 17/28] kmp: make GpsLayerHandle public --- .../mapscore/kmp/core/feature/map/interop/MapImplementations.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt index 79197c190..fa9775b3c 100644 --- a/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt +++ b/kmp/androidMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/MapImplementations.kt @@ -235,7 +235,7 @@ private fun MapscoreGpsMode.asShared(): GpsMode = when (this) { MapscoreGpsMode.FOLLOW_AND_TURN -> GpsMode.FOLLOW } -internal data class GpsLayerHandle( +data class GpsLayerHandle( val layer: GpsLayer, val locationProvider: LocationProviderInterface, ) From a4f3341766d1121c1777a876cadd50fee813be90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 21:55:32 +0100 Subject: [PATCH 18/28] fix(kmp): add ios arm64 cinterop and spm fallback --- build.gradle.kts | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7c5d3ca82..c7318e29d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -57,11 +57,9 @@ kotlin { ) iosTargets.forEach { iosTarget -> - if (iosTarget.name == "iosSimulatorArm64") { - iosTarget.compilations { - val main by getting { - cinterops.create(mapCoreCinteropName) - } + iosTarget.compilations { + val main by getting { + cinterops.create(mapCoreCinteropName) } } } @@ -136,6 +134,33 @@ val mapCoreSpmBuiltDir = project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/scratch/arm64 x86_64-apple-ios-simulator/release").get().asFile mapCoreSpmBuiltDir.mkdirs() +val mapCoreSpmDeviceDir = + project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/scratch/arm64-apple-ios/release") +val mapCoreSpmSimulatorDir = + project.layout.buildDirectory.dir("spmKmpPlugin/MapCoreKmp/scratch/arm64-apple-ios-simulator/release") + +afterEvaluate { + val deviceTaskName = "SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackageIosArm64" + if (tasks.findByName(deviceTaskName) != null) return@afterEvaluate + + val simulatorTaskName = "SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackageIosSimulatorArm64" + tasks.register(deviceTaskName) { + group = "io.github.frankois944.spmForKmp.tasks" + description = "Fallback: copy simulator SwiftPM output for iOS device metal compilation" + dependsOn(simulatorTaskName) + doLast { + val sourceDir = mapCoreSpmSimulatorDir.get().asFile + if (!sourceDir.exists()) return@doLast + val targetDir = mapCoreSpmDeviceDir.get().asFile + targetDir.mkdirs() + copy { + from(sourceDir) + into(targetDir) + } + } + } +} + abstract class CompileMapCoreMetallibTask : DefaultTask() { @get:Input abstract val sdk: Property From 09becdb282a10a56ce77fe4dda02f0de43218fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Mon, 26 Jan 2026 22:06:26 +0100 Subject: [PATCH 19/28] fix(kmp): avoid duplicate spm task registration --- build.gradle.kts | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index c7318e29d..a28e67a6f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -141,23 +141,29 @@ val mapCoreSpmSimulatorDir = afterEvaluate { val deviceTaskName = "SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackageIosArm64" - if (tasks.findByName(deviceTaskName) != null) return@afterEvaluate - val simulatorTaskName = "SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackageIosSimulatorArm64" - tasks.register(deviceTaskName) { - group = "io.github.frankois944.spmForKmp.tasks" - description = "Fallback: copy simulator SwiftPM output for iOS device metal compilation" - dependsOn(simulatorTaskName) - doLast { - val sourceDir = mapCoreSpmSimulatorDir.get().asFile - if (!sourceDir.exists()) return@doLast - val targetDir = mapCoreSpmDeviceDir.get().asFile - targetDir.mkdirs() - copy { - from(sourceDir) - into(targetDir) + if (tasks.findByName(deviceTaskName) != null) return@afterEvaluate + runCatching { + tasks.register(deviceTaskName) { + group = "io.github.frankois944.spmForKmp.tasks" + description = "Fallback: copy simulator SwiftPM output for iOS device metal compilation" + dependsOn(simulatorTaskName) + doLast { + val sourceDir = mapCoreSpmSimulatorDir.get().asFile + if (!sourceDir.exists()) return@doLast + val targetDir = mapCoreSpmDeviceDir.get().asFile + targetDir.mkdirs() + copy { + from(sourceDir) + into(targetDir) + } } } + }.onFailure { error -> + val message = error.message.orEmpty() + if (!message.contains("already exists")) { + throw error + } } } From 328de4c467c6b3fe88ff1715069afa686eb8d3de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Tue, 27 Jan 2026 06:49:48 +0100 Subject: [PATCH 20/28] update gradle --- .gitignore | 1 + build.gradle.kts | 19 +++++-------------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 9407b7681..e93dc8b12 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,4 @@ compile_commands.json cmake-build-test /local.properties +/.gradle diff --git a/build.gradle.kts b/build.gradle.kts index a28e67a6f..354f73fd1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ val mapCoreMetalTargetDevice = providers.environmentVariable("MAPCORE_METAL_TARG plugins { id("org.jetbrains.kotlin.multiplatform") version "2.3.0" - id("com.android.library") version "8.12.0" + id("com.android.kotlin.multiplatform.library") version "8.12.0" id("io.github.frankois944.spmForKmp") version "1.4.6" } @@ -44,10 +44,13 @@ kotlin { freeCompilerArgs.add("-Xexpect-actual-classes") // Opt-in for expect/actual classes } - androidTarget { + android { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) } + namespace = "io.openmobilemaps.mapscore.kmp" + compileSdk = 36 + minSdk = 31 } val mapCoreCinteropName = "MapCoreKmp" @@ -295,15 +298,3 @@ tasks.matching { it.name == "compileKotlinIosSimulatorArm64" } .configureEach { dependsOn(compileMapCoreMetallibIosSimulator) } tasks.matching { it.name == "compileKotlinIosArm64" } .configureEach { dependsOn(compileMapCoreMetallibIosArm64) } - -android { - namespace = "io.openmobilemaps.mapscore.kmp" - compileSdk = 36 - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - defaultConfig { - minSdk = 31 - } -} From 0a644ec18049c965c6b054b9dad6fa378124cbb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Tue, 27 Jan 2026 07:06:09 +0100 Subject: [PATCH 21/28] remove dependsOn --- build.gradle.kts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 354f73fd1..c63463393 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -84,16 +84,13 @@ kotlin { implementation("ch.ubique.android:djinni-support-lib:1.1.1") } } - val iosMain by creating { - dependsOn(commonMain) + val iosMain by getting { kotlin.srcDir("kmp/iosMain/kotlin") } val iosArm64Main by getting { - dependsOn(iosMain) languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") } val iosSimulatorArm64Main by getting { - dependsOn(iosMain) languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") } } From 555bd913dc5d36cd2ceabf05b520a28943913aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Tue, 27 Jan 2026 07:20:06 +0100 Subject: [PATCH 22/28] targetHierarchy.default() --- build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index c63463393..ab7c12b16 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,6 +44,8 @@ kotlin { freeCompilerArgs.add("-Xexpect-actual-classes") // Opt-in for expect/actual classes } + applyDefaultHierarchyTemplate() + android { compilerOptions { jvmTarget.set(JvmTarget.JVM_17) From c37b8e62ec9387631fc2455c4e10a171be92a739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Tue, 27 Jan 2026 09:25:54 +0100 Subject: [PATCH 23/28] improve kmp setup --- build.gradle.kts | 31 +++++++++++++++++++++++++++++++ gradle.properties | 1 + 2 files changed, 32 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index ab7c12b16..6cf8cbe59 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -125,6 +125,37 @@ swiftPackageConfig { } } +// Avoid overlapping Package.resolved outputs between per-target SwiftPM compile tasks. +tasks + .matching { it.name.startsWith("SwiftPackageConfigAppleMapCoreKmpCompileSwiftPackage") } + .configureEach { + val packageResolveFileGetter = javaClass.methods.firstOrNull { it.name == "getPackageResolveFile" } + val packageResolveFile = + project.layout.buildDirectory.file( + "spmKmpPlugin/MapCoreKmp/package-resolved/${name}/Package.resolved", + ) + val packageResolveProperty = + packageResolveFileGetter + ?.invoke(this) as? RegularFileProperty + packageResolveProperty?.set(packageResolveFile) + + doLast { + val sourceFile = + project.layout.buildDirectory + .file("spmKmpPlugin/MapCoreKmp/Package.resolved") + .get() + .asFile + if (!sourceFile.exists()) return@doLast + val targetFile = + project.layout.buildDirectory + .file("spmKmpPlugin/MapCoreKmp/package-resolved/${name}/Package.resolved") + .get() + .asFile + targetFile.parentFile.mkdirs() + sourceFile.copyTo(targetFile, overwrite = true) + } + } + tasks.withType().configureEach { if (name.contains("MapCoreKmp")) { settings.compilerOpts("-I$mapCoreCheckoutPath/external/djinni/support-lib/objc") diff --git a/gradle.properties b/gradle.properties index 5bac8ac50..b65e5f51d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,2 @@ android.useAndroidX=true +kotlin.mpp.enableCInteropCommonization=true From 7fa24871d078389c3e51e6bdf78eeb0a22900db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Tue, 27 Jan 2026 10:02:26 +0100 Subject: [PATCH 24/28] configure tasks --- build.gradle.kts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 6cf8cbe59..409d3e217 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -324,6 +324,11 @@ val compileMapCoreMetallibIosArm64 = tasks.register( metallibFile.set(bundleDir.map { it.file("default.metallib") }) } +tasks.matching { it.name == "SwiftPackageConfigAppleMapCoreKmpCopyPackageResourcesIosSimulatorArm64" } + .configureEach { dependsOn(compileMapCoreMetallibIosSimulator) } +tasks.matching { it.name == "SwiftPackageConfigAppleMapCoreKmpCopyPackageResourcesIosArm64" } + .configureEach { dependsOn(compileMapCoreMetallibIosArm64) } + tasks.matching { it.name == "compileKotlinIosSimulatorArm64" } .configureEach { dependsOn(compileMapCoreMetallibIosSimulator) } tasks.matching { it.name == "compileKotlinIosArm64" } From f7ba2aedb624ad45bdd74e837a48ff81d13fea49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Tue, 27 Jan 2026 13:11:18 +0100 Subject: [PATCH 25/28] kmp: opt in to ExperimentalForeignApi for Coord --- .../mapscore/kmp/core/feature/map/interop/Coord.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt index 4e299f4c5..799b5220c 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt @@ -1,3 +1,5 @@ +@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) + package io.openmobilemaps.mapscore.kmp.feature.map.interop import MapCoreSharedModule.MCCoord From 3c6206c362cff366e1f062743a5176094eb7d2c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Tue, 27 Jan 2026 13:12:21 +0100 Subject: [PATCH 26/28] kmp: opt in to ExperimentalForeignApi in iosMain --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index 409d3e217..8a78f5a6f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -88,6 +88,7 @@ kotlin { } val iosMain by getting { kotlin.srcDir("kmp/iosMain/kotlin") + languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") } val iosArm64Main by getting { languageSettings.optIn("kotlinx.cinterop.ExperimentalForeignApi") From 173c8dde58ce9eced5ea450c1972709824120afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20Ma=CC=88rki?= Date: Tue, 27 Jan 2026 13:15:26 +0100 Subject: [PATCH 27/28] kmp: drop redundant ExperimentalForeignApi opt-in --- .../mapscore/kmp/core/feature/map/interop/Coord.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt index 799b5220c..4e299f4c5 100644 --- a/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt +++ b/kmp/iosMain/kotlin/io/openmobilemaps/mapscore/kmp/core/feature/map/interop/Coord.kt @@ -1,5 +1,3 @@ -@file:OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) - package io.openmobilemaps.mapscore.kmp.feature.map.interop import MapCoreSharedModule.MCCoord From fa80dbb76b0c29b501d518afdac4ff9bbb311d1e Mon Sep 17 00:00:00 2001 From: Christoph Maurhofer Date: Wed, 28 Jan 2026 14:18:34 +0100 Subject: [PATCH 28/28] Adjust android multiplatform plugin version, adjust minSDK for android --- .gitignore | 1 + build.gradle.kts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index e93dc8b12..51ad34768 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,4 @@ compile_commands.json cmake-build-test /local.properties /.gradle +/.kotlin diff --git a/build.gradle.kts b/build.gradle.kts index 8a78f5a6f..21e705874 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ import org.gradle.api.tasks.TaskAction import org.gradle.process.ExecOperations import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget import org.jetbrains.kotlin.gradle.tasks.CInteropProcess import java.net.URI import javax.inject.Inject @@ -34,7 +35,7 @@ val mapCoreMetalTargetDevice = providers.environmentVariable("MAPCORE_METAL_TARG plugins { id("org.jetbrains.kotlin.multiplatform") version "2.3.0" - id("com.android.kotlin.multiplatform.library") version "8.12.0" + id("com.android.kotlin.multiplatform.library") version "8.13.2" id("io.github.frankois944.spmForKmp") version "1.4.6" } @@ -52,7 +53,7 @@ kotlin { } namespace = "io.openmobilemaps.mapscore.kmp" compileSdk = 36 - minSdk = 31 + minSdk = 28 } val mapCoreCinteropName = "MapCoreKmp"