@maydon_tech/react-native-nitro-maps 0.1.3 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/NitroMap.podspec +1 -1
- package/README.md +82 -9
- package/android/CMakeLists.txt +4 -1
- package/android/gradle.properties +4 -4
- package/android/src/main/cpp/ClusterEngineJNI.cpp +198 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMap.kt +397 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapConfig.kt +53 -0
- package/android/src/main/{java → kotlin}/com/margelo/nitro/nitromap/NitroMapPackage.kt +4 -4
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/NitroMapView.kt +73 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/UserLocationManager.kt +295 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusterIconRenderer.kt +111 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/ClusteringManager.kt +104 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +166 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerIconFactory.kt +303 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/MarkerSelectionHandler.kt +72 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/markers/PriceMarkerRenderer.kt +159 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderFactory.kt +24 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +128 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapDelegate.kt +317 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Clustering.kt +524 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Markers.kt +358 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+Overlays.kt +272 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider+UserLocation.kt +296 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/GoogleMapProvider.kt +815 -0
- package/android/src/main/kotlin/com/margelo/nitro/nitromap/providers/google/MarkerTagData.kt +19 -0
- package/ios/Clustering/ClusterIconRenderer.swift +3 -3
- package/ios/Location/NitroLocationManager.swift +116 -0
- package/ios/MarkerRenderer/MarkerIconFactory.swift +1 -3
- package/ios/MarkerRenderer/PriceMarkerRenderer.swift +10 -6
- package/ios/NitroMap.swift +279 -13
- package/ios/NitroMapConfig/NitroMapConfig.swift +45 -0
- package/ios/Providers/{GoogleMapDelegate.swift → Google/GoogleMapDelegate.swift} +48 -23
- package/ios/Providers/Google/GoogleMapProvider+Camera.swift +180 -0
- package/ios/Providers/Google/GoogleMapProvider+Clustering.swift +541 -0
- package/ios/Providers/Google/GoogleMapProvider+Markers.swift +270 -0
- package/ios/Providers/Google/GoogleMapProvider+Overlays.swift +245 -0
- package/ios/Providers/Google/GoogleMapProvider+UserLocation.swift +180 -0
- package/ios/Providers/Google/GoogleMapProvider.swift +342 -0
- package/ios/Providers/MapProviderFactory.swift +17 -0
- package/ios/Providers/MapProviderProtocol.swift +48 -1
- package/ios/Shared/ClusterConfig+Factory.swift +2 -2
- package/ios/Shared/MapStyleProvider.swift +6 -4
- package/ios/Shared/MarkerSelectionHandler.swift +4 -1
- package/ios/Utils/ColorValueExtension.swift +46 -67
- package/lib/module/components/ImageMarker.js +39 -29
- package/lib/module/components/ImageMarker.js.map +1 -1
- package/lib/module/components/Marker.js +118 -0
- package/lib/module/components/Marker.js.map +1 -0
- package/lib/module/components/NitroCircle.js +92 -0
- package/lib/module/components/NitroCircle.js.map +1 -0
- package/lib/module/components/NitroMap.js +216 -76
- package/lib/module/components/NitroMap.js.map +1 -1
- package/lib/module/components/NitroPolygon.js +135 -0
- package/lib/module/components/NitroPolygon.js.map +1 -0
- package/lib/module/components/NitroPolyline.js +115 -0
- package/lib/module/components/NitroPolyline.js.map +1 -0
- package/lib/module/components/PriceMarker.js +16 -29
- package/lib/module/components/PriceMarker.js.map +1 -1
- package/lib/module/context/NitroMapContext.js.map +1 -1
- package/lib/module/hooks/useNitroCircle.js +18 -0
- package/lib/module/hooks/useNitroCircle.js.map +1 -0
- package/lib/module/hooks/useNitroMarker.js +26 -9
- package/lib/module/hooks/useNitroMarker.js.map +1 -1
- package/lib/module/hooks/useNitroOverlay.js +59 -0
- package/lib/module/hooks/useNitroOverlay.js.map +1 -0
- package/lib/module/hooks/useNitroPolygon.js +18 -0
- package/lib/module/hooks/useNitroPolygon.js.map +1 -0
- package/lib/module/hooks/useNitroPolyline.js +18 -0
- package/lib/module/hooks/useNitroPolyline.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/types/overlay.js +4 -0
- package/lib/module/types/overlay.js.map +1 -0
- package/lib/module/types/theme.js +4 -0
- package/lib/module/types/theme.js.map +1 -0
- package/lib/module/utils/colors.js +41 -13
- package/lib/module/utils/colors.js.map +1 -1
- package/lib/module/utils/validation.js +45 -0
- package/lib/module/utils/validation.js.map +1 -0
- package/lib/typescript/src/components/ImageMarker.d.ts.map +1 -1
- package/lib/typescript/src/components/Marker.d.ts +34 -0
- package/lib/typescript/src/components/Marker.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroCircle.d.ts +70 -0
- package/lib/typescript/src/components/NitroCircle.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroMap.d.ts +60 -3
- package/lib/typescript/src/components/NitroMap.d.ts.map +1 -1
- package/lib/typescript/src/components/NitroPolygon.d.ts +86 -0
- package/lib/typescript/src/components/NitroPolygon.d.ts.map +1 -0
- package/lib/typescript/src/components/NitroPolyline.d.ts +84 -0
- package/lib/typescript/src/components/NitroPolyline.d.ts.map +1 -0
- package/lib/typescript/src/components/PriceMarker.d.ts +0 -5
- package/lib/typescript/src/components/PriceMarker.d.ts.map +1 -1
- package/lib/typescript/src/context/NitroMapContext.d.ts +2 -0
- package/lib/typescript/src/context/NitroMapContext.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useNitroCircle.d.ts +7 -0
- package/lib/typescript/src/hooks/useNitroCircle.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useNitroMarker.d.ts +20 -0
- package/lib/typescript/src/hooks/useNitroMarker.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useNitroOverlay.d.ts +26 -0
- package/lib/typescript/src/hooks/useNitroOverlay.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useNitroPolygon.d.ts +7 -0
- package/lib/typescript/src/hooks/useNitroPolygon.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useNitroPolyline.d.ts +7 -0
- package/lib/typescript/src/hooks/useNitroPolyline.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +15 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/specs/NitroMap.nitro.d.ts +248 -6
- package/lib/typescript/src/specs/NitroMap.nitro.d.ts.map +1 -1
- package/lib/typescript/src/types/map.d.ts +34 -4
- package/lib/typescript/src/types/map.d.ts.map +1 -1
- package/lib/typescript/src/types/marker.d.ts +24 -36
- package/lib/typescript/src/types/marker.d.ts.map +1 -1
- package/lib/typescript/src/types/overlay.d.ts +75 -0
- package/lib/typescript/src/types/overlay.d.ts.map +1 -0
- package/lib/typescript/src/types/theme.d.ts +93 -0
- package/lib/typescript/src/types/theme.d.ts.map +1 -0
- package/lib/typescript/src/utils/colors.d.ts +6 -8
- package/lib/typescript/src/utils/colors.d.ts.map +1 -1
- package/lib/typescript/src/utils/validation.d.ts +12 -0
- package/lib/typescript/src/utils/validation.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JCircleData.hpp +94 -0
- package/nitrogen/generated/android/c++/JClusterConfig.hpp +5 -7
- package/nitrogen/generated/android/c++/JFunc_void_UserLocationChangeEvent.hpp +79 -0
- package/nitrogen/generated/android/c++/JFunc_void_UserTrackingMode.hpp +77 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__string.hpp +76 -0
- package/nitrogen/generated/android/c++/JHybridNitroMapSpec.cpp +328 -21
- package/nitrogen/generated/android/c++/JHybridNitroMapSpec.hpp +53 -2
- package/nitrogen/generated/android/c++/JMarkerAnimation.hpp +3 -6
- package/nitrogen/generated/android/c++/JMarkerData.hpp +15 -3
- package/nitrogen/generated/android/c++/JPolygonData.hpp +149 -0
- package/nitrogen/generated/android/c++/JPolylineData.hpp +113 -0
- package/nitrogen/generated/android/c++/JUserLocationChangeEvent.hpp +70 -0
- package/nitrogen/generated/android/c++/JUserTrackingMode.hpp +62 -0
- package/nitrogen/generated/android/c++/views/JHybridNitroMapStateUpdater.cpp +72 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/CircleData.kt +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/ClusterConfig.kt +4 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserLocationChangeEvent.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_UserTrackingMode.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/Func_void_std__string.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/HybridNitroMapSpec.kt +228 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerAnimation.kt +1 -2
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/MarkerData.kt +12 -3
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolygonData.kt +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/PolylineData.kt +62 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/UserLocationChangeEvent.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitromap/{ClusterAnimationStyle.kt → UserTrackingMode.kt} +6 -8
- package/nitrogen/generated/android/nitromapOnLoad.cpp +6 -0
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.cpp +24 -0
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Bridge.hpp +175 -17
- package/nitrogen/generated/ios/NitroMap-Swift-Cxx-Umbrella.hpp +15 -3
- package/nitrogen/generated/ios/c++/HybridNitroMapSpecSwift.hpp +249 -16
- package/nitrogen/generated/ios/c++/views/HybridNitroMapComponent.mm +90 -5
- package/nitrogen/generated/ios/swift/CircleData.swift +143 -0
- package/nitrogen/generated/ios/swift/ClusterConfig.swift +22 -15
- package/nitrogen/generated/ios/swift/Func_void_UserLocationChangeEvent.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_UserTrackingMode.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridNitroMapSpec.swift +35 -1
- package/nitrogen/generated/ios/swift/HybridNitroMapSpec_cxx.swift +582 -8
- package/nitrogen/generated/ios/swift/MarkerAnimation.swift +4 -8
- package/nitrogen/generated/ios/swift/MarkerData.swift +54 -2
- package/nitrogen/generated/ios/swift/PolygonData.swift +179 -0
- package/nitrogen/generated/ios/swift/PolylineData.swift +155 -0
- package/nitrogen/generated/ios/swift/UserLocationChangeEvent.swift +69 -0
- package/nitrogen/generated/ios/swift/UserTrackingMode.swift +44 -0
- package/nitrogen/generated/shared/c++/CircleData.hpp +113 -0
- package/nitrogen/generated/shared/c++/ClusterConfig.hpp +5 -8
- package/nitrogen/generated/shared/c++/HybridNitroMapSpec.cpp +53 -2
- package/nitrogen/generated/shared/c++/HybridNitroMapSpec.hpp +75 -6
- package/nitrogen/generated/shared/c++/MarkerAnimation.hpp +4 -8
- package/nitrogen/generated/shared/c++/MarkerData.hpp +14 -2
- package/nitrogen/generated/shared/c++/PolygonData.hpp +114 -0
- package/nitrogen/generated/shared/c++/PolylineData.hpp +114 -0
- package/nitrogen/generated/shared/c++/UserLocationChangeEvent.hpp +88 -0
- package/nitrogen/generated/shared/c++/UserTrackingMode.hpp +80 -0
- package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.cpp +216 -12
- package/nitrogen/generated/shared/c++/views/HybridNitroMapComponent.hpp +23 -1
- package/nitrogen/generated/shared/json/NitroMapConfig.json +18 -1
- package/package.json +36 -5
- package/src/components/ImageMarker.tsx +58 -42
- package/src/components/Marker.tsx +161 -0
- package/src/components/NitroCircle.tsx +183 -0
- package/src/components/NitroMap.tsx +328 -78
- package/src/components/NitroPolygon.tsx +229 -0
- package/src/components/NitroPolyline.tsx +208 -0
- package/src/components/PriceMarker.tsx +23 -48
- package/src/context/NitroMapContext.tsx +4 -0
- package/src/hooks/useNitroCircle.ts +25 -0
- package/src/hooks/useNitroMarker.ts +49 -10
- package/src/hooks/useNitroOverlay.ts +68 -0
- package/src/hooks/useNitroPolygon.ts +25 -0
- package/src/hooks/useNitroPolyline.ts +25 -0
- package/src/index.tsx +23 -2
- package/src/specs/NitroMap.nitro.ts +294 -5
- package/src/types/map.ts +36 -4
- package/src/types/marker.ts +24 -44
- package/src/types/overlay.ts +77 -0
- package/src/types/theme.ts +101 -0
- package/src/utils/colors.ts +48 -16
- package/src/utils/validation.ts +69 -0
- package/android/src/main/java/com/margelo/nitro/nitromap/ClusterIconGenerator.kt +0 -108
- package/android/src/main/java/com/margelo/nitro/nitromap/ColorUtils.kt +0 -63
- package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMap.kt +0 -408
- package/android/src/main/java/com/margelo/nitro/nitromap/HybridNitroMapConfig.kt +0 -68
- package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconCache.kt +0 -176
- package/android/src/main/java/com/margelo/nitro/nitromap/MarkerIconFactory.kt +0 -252
- package/android/src/main/java/com/margelo/nitro/nitromap/clustering/NitroClusterEngine.kt +0 -252
- package/android/src/main/java/com/margelo/nitro/nitromap/clustering/QuadTree.kt +0 -195
- package/android/src/main/java/com/margelo/nitro/nitromap/providers/GoogleMapProvider.kt +0 -912
- package/android/src/main/java/com/margelo/nitro/nitromap/providers/MapProviderInterface.kt +0 -70
- package/cpp/QuadTree.hpp +0 -246
- package/ios/NitroMapConfig/HybridNitroMapConfig.swift +0 -33
- package/ios/Providers/GoogleMapProvider+Camera.swift +0 -164
- package/ios/Providers/GoogleMapProvider.swift +0 -924
- package/nitrogen/generated/android/c++/JClusterAnimationStyle.hpp +0 -68
- package/nitrogen/generated/ios/swift/ClusterAnimationStyle.swift +0 -52
- package/nitrogen/generated/shared/c++/ClusterAnimationStyle.hpp +0 -88
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
package com.margelo.nitro.nitromap
|
|
2
|
+
|
|
3
|
+
import android.Manifest
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.pm.PackageManager
|
|
6
|
+
import android.hardware.Sensor
|
|
7
|
+
import android.hardware.SensorEvent
|
|
8
|
+
import android.hardware.SensorEventListener
|
|
9
|
+
import android.hardware.SensorManager
|
|
10
|
+
import android.location.Location
|
|
11
|
+
import android.os.Handler
|
|
12
|
+
import android.os.Looper
|
|
13
|
+
import android.util.Log
|
|
14
|
+
import androidx.core.content.ContextCompat
|
|
15
|
+
import com.google.android.gms.location.FusedLocationProviderClient
|
|
16
|
+
import com.google.android.gms.location.LocationCallback
|
|
17
|
+
import com.google.android.gms.location.LocationRequest
|
|
18
|
+
import com.google.android.gms.location.LocationResult
|
|
19
|
+
import com.google.android.gms.location.LocationServices
|
|
20
|
+
import com.google.android.gms.location.Priority
|
|
21
|
+
import kotlin.math.abs
|
|
22
|
+
import kotlin.math.atan2
|
|
23
|
+
import kotlin.math.cos
|
|
24
|
+
import kotlin.math.sin
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Android equivalent of iOS NitroLocationManager.
|
|
28
|
+
*
|
|
29
|
+
* Wraps FusedLocationProviderClient for position updates and
|
|
30
|
+
* SensorManager for compass heading (Android has no CLHeading equivalent).
|
|
31
|
+
*
|
|
32
|
+
* Heading is smoothed with a low-pass filter on raw sensor data and
|
|
33
|
+
* circular exponential smoothing on the computed azimuth, matching
|
|
34
|
+
* the stability of iOS CLHeading.trueHeading.
|
|
35
|
+
*
|
|
36
|
+
* Thread safety: All callbacks fire on the main thread.
|
|
37
|
+
*/
|
|
38
|
+
class UserLocationManager(private val context: Context) {
|
|
39
|
+
|
|
40
|
+
companion object {
|
|
41
|
+
private const val TAG = "NitroMap"
|
|
42
|
+
private const val UPDATE_INTERVAL_MS = 1000L
|
|
43
|
+
private const val FASTEST_INTERVAL_MS = 500L
|
|
44
|
+
|
|
45
|
+
/** Low-pass filter alpha for raw sensor arrays (0 = ignore new, 1 = no filter) */
|
|
46
|
+
private const val SENSOR_ALPHA = 0.15f
|
|
47
|
+
|
|
48
|
+
/** Exponential smoothing alpha for the final heading angle */
|
|
49
|
+
private const val HEADING_ALPHA = 0.25f
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Minimum heading change (degrees) before firing an independent heading
|
|
53
|
+
* update — mirrors iOS CLLocationManager.headingFilter = 1.
|
|
54
|
+
*/
|
|
55
|
+
private const val HEADING_FILTER_DEGREES = 2f
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Minimum interval (ms) between independent heading-only updates.
|
|
59
|
+
* Prevents overwhelming the main thread when combined with 3000+ markers
|
|
60
|
+
* and clustering. 200ms = max 5 heading updates/second.
|
|
61
|
+
*/
|
|
62
|
+
private const val MIN_HEADING_INTERVAL_MS = 200L
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// MARK: - Callbacks
|
|
66
|
+
|
|
67
|
+
/** Called with (location, headingDegrees) — heading may be null if sensors unavailable */
|
|
68
|
+
var onLocationUpdate: ((Location, Float?) -> Unit)? = null
|
|
69
|
+
|
|
70
|
+
/** Called with (errorCode, errorMessage) */
|
|
71
|
+
var onLocationError: ((String, String) -> Unit)? = null
|
|
72
|
+
|
|
73
|
+
// MARK: - Location Provider
|
|
74
|
+
|
|
75
|
+
private var fusedClient: FusedLocationProviderClient? = null
|
|
76
|
+
private var locationCallback: LocationCallback? = null
|
|
77
|
+
private var isUpdating = false
|
|
78
|
+
|
|
79
|
+
/** Latest location — kept so heading-only updates can re-fire with position */
|
|
80
|
+
private var latestLocation: Location? = null
|
|
81
|
+
|
|
82
|
+
// MARK: - Heading (Compass) via Sensors
|
|
83
|
+
|
|
84
|
+
private var sensorManager: SensorManager? = null
|
|
85
|
+
private var accelerometerSensor: Sensor? = null
|
|
86
|
+
private var magneticSensor: Sensor? = null
|
|
87
|
+
private var sensorListener: SensorEventListener? = null
|
|
88
|
+
|
|
89
|
+
/** Low-pass filtered gravity values */
|
|
90
|
+
private var filteredGravity: FloatArray? = null
|
|
91
|
+
/** Low-pass filtered magnetic field values */
|
|
92
|
+
private var filteredGeomagnetic: FloatArray? = null
|
|
93
|
+
|
|
94
|
+
/** Smoothed heading (degrees, 0-360) — null until first valid reading */
|
|
95
|
+
private var smoothedHeading: Float? = null
|
|
96
|
+
|
|
97
|
+
/** Last heading value that was delivered to the callback */
|
|
98
|
+
private var lastDeliveredHeading: Float? = null
|
|
99
|
+
|
|
100
|
+
/** Timestamp of the last independent heading-only update delivery */
|
|
101
|
+
private var lastHeadingDeliveryTime: Long = 0L
|
|
102
|
+
|
|
103
|
+
// MARK: - Public API
|
|
104
|
+
|
|
105
|
+
fun startUpdating() {
|
|
106
|
+
if (isUpdating) return
|
|
107
|
+
|
|
108
|
+
// Check permission
|
|
109
|
+
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
|
|
110
|
+
!= PackageManager.PERMISSION_GRANTED &&
|
|
111
|
+
ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
|
|
112
|
+
!= PackageManager.PERMISSION_GRANTED
|
|
113
|
+
) {
|
|
114
|
+
Log.w(TAG, "Location permission not granted")
|
|
115
|
+
onLocationError?.invoke("PERMISSION_DENIED", "Location permission not granted")
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
isUpdating = true
|
|
120
|
+
|
|
121
|
+
// Start location updates
|
|
122
|
+
val client = LocationServices.getFusedLocationProviderClient(context)
|
|
123
|
+
fusedClient = client
|
|
124
|
+
|
|
125
|
+
val request = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, UPDATE_INTERVAL_MS)
|
|
126
|
+
.setMinUpdateIntervalMillis(FASTEST_INTERVAL_MS)
|
|
127
|
+
.build()
|
|
128
|
+
|
|
129
|
+
val callback = object : LocationCallback() {
|
|
130
|
+
override fun onLocationResult(result: LocationResult) {
|
|
131
|
+
val location = result.lastLocation ?: return
|
|
132
|
+
latestLocation = location
|
|
133
|
+
onLocationUpdate?.invoke(location, smoothedHeading)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
locationCallback = callback
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
client.requestLocationUpdates(request, callback, Looper.getMainLooper())
|
|
140
|
+
Log.d(TAG, "Location updates started")
|
|
141
|
+
} catch (e: SecurityException) {
|
|
142
|
+
Log.e(TAG, "Location security exception: ${e.message}")
|
|
143
|
+
onLocationError?.invoke("PERMISSION_DENIED", "Location permission revoked: ${e.message}")
|
|
144
|
+
isUpdating = false
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Start heading sensor
|
|
149
|
+
startHeadingSensor()
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fun stopUpdating() {
|
|
153
|
+
if (!isUpdating) return
|
|
154
|
+
isUpdating = false
|
|
155
|
+
|
|
156
|
+
locationCallback?.let { callback ->
|
|
157
|
+
fusedClient?.removeLocationUpdates(callback)
|
|
158
|
+
}
|
|
159
|
+
locationCallback = null
|
|
160
|
+
fusedClient = null
|
|
161
|
+
|
|
162
|
+
stopHeadingSensor()
|
|
163
|
+
latestLocation = null
|
|
164
|
+
Log.d(TAG, "Location updates stopped")
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// MARK: - Heading Sensor
|
|
168
|
+
|
|
169
|
+
private fun startHeadingSensor() {
|
|
170
|
+
val sm = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager ?: return
|
|
171
|
+
sensorManager = sm
|
|
172
|
+
|
|
173
|
+
accelerometerSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
|
|
174
|
+
magneticSensor = sm.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD)
|
|
175
|
+
|
|
176
|
+
if (accelerometerSensor == null || magneticSensor == null) {
|
|
177
|
+
Log.w(TAG, "Compass sensors not available")
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
val listener = object : SensorEventListener {
|
|
182
|
+
override fun onSensorChanged(event: SensorEvent) {
|
|
183
|
+
when (event.sensor.type) {
|
|
184
|
+
Sensor.TYPE_ACCELEROMETER ->
|
|
185
|
+
filteredGravity = lowPassFilter(event.values, filteredGravity)
|
|
186
|
+
Sensor.TYPE_MAGNETIC_FIELD ->
|
|
187
|
+
filteredGeomagnetic = lowPassFilter(event.values, filteredGeomagnetic)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
val g = filteredGravity ?: return
|
|
191
|
+
val m = filteredGeomagnetic ?: return
|
|
192
|
+
|
|
193
|
+
val R = FloatArray(9)
|
|
194
|
+
val I = FloatArray(9)
|
|
195
|
+
if (!SensorManager.getRotationMatrix(R, I, g, m)) return
|
|
196
|
+
|
|
197
|
+
val orientation = FloatArray(3)
|
|
198
|
+
SensorManager.getOrientation(R, orientation)
|
|
199
|
+
|
|
200
|
+
// orientation[0] = azimuth in radians
|
|
201
|
+
var rawHeading = Math.toDegrees(orientation[0].toDouble()).toFloat()
|
|
202
|
+
if (rawHeading < 0) rawHeading += 360f
|
|
203
|
+
|
|
204
|
+
// Apply circular exponential smoothing
|
|
205
|
+
val prev = smoothedHeading
|
|
206
|
+
smoothedHeading = if (prev == null) {
|
|
207
|
+
rawHeading
|
|
208
|
+
} else {
|
|
209
|
+
circularSmooth(prev, rawHeading, HEADING_ALPHA)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Fire heading update independently (mirrors iOS didUpdateHeading)
|
|
213
|
+
// only when heading changed by more than the filter threshold
|
|
214
|
+
// AND enough time has passed (throttle to avoid flooding main thread)
|
|
215
|
+
val delivered = lastDeliveredHeading
|
|
216
|
+
val current = smoothedHeading ?: return
|
|
217
|
+
val now = System.currentTimeMillis()
|
|
218
|
+
if ((delivered == null || circularDistance(delivered, current) >= HEADING_FILTER_DEGREES)
|
|
219
|
+
&& (now - lastHeadingDeliveryTime >= MIN_HEADING_INTERVAL_MS)
|
|
220
|
+
) {
|
|
221
|
+
lastDeliveredHeading = current
|
|
222
|
+
lastHeadingDeliveryTime = now
|
|
223
|
+
val loc = latestLocation ?: return
|
|
224
|
+
onLocationUpdate?.invoke(loc, current)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
|
|
229
|
+
}
|
|
230
|
+
sensorListener = listener
|
|
231
|
+
|
|
232
|
+
val mainHandler = Handler(Looper.getMainLooper())
|
|
233
|
+
sm.registerListener(listener, accelerometerSensor, SensorManager.SENSOR_DELAY_UI, mainHandler)
|
|
234
|
+
sm.registerListener(listener, magneticSensor, SensorManager.SENSOR_DELAY_UI, mainHandler)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private fun stopHeadingSensor() {
|
|
238
|
+
sensorListener?.let { listener ->
|
|
239
|
+
sensorManager?.unregisterListener(listener)
|
|
240
|
+
}
|
|
241
|
+
sensorListener = null
|
|
242
|
+
sensorManager = null
|
|
243
|
+
accelerometerSensor = null
|
|
244
|
+
magneticSensor = null
|
|
245
|
+
filteredGravity = null
|
|
246
|
+
filteredGeomagnetic = null
|
|
247
|
+
smoothedHeading = null
|
|
248
|
+
lastDeliveredHeading = null
|
|
249
|
+
lastHeadingDeliveryTime = 0L
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// MARK: - Signal Processing Helpers
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Low-pass filter for raw sensor arrays.
|
|
256
|
+
* Smooths out high-frequency jitter in accelerometer/magnetometer readings.
|
|
257
|
+
*/
|
|
258
|
+
private fun lowPassFilter(input: FloatArray, output: FloatArray?): FloatArray {
|
|
259
|
+
if (output == null) return input.clone()
|
|
260
|
+
for (i in input.indices) {
|
|
261
|
+
output[i] = output[i] + SENSOR_ALPHA * (input[i] - output[i])
|
|
262
|
+
}
|
|
263
|
+
return output
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Circular exponential smoothing that handles the 0°/360° wraparound correctly.
|
|
268
|
+
* Uses sin/cos decomposition to avoid the discontinuity at north.
|
|
269
|
+
*/
|
|
270
|
+
private fun circularSmooth(prev: Float, next: Float, alpha: Float): Float {
|
|
271
|
+
val prevRad = Math.toRadians(prev.toDouble())
|
|
272
|
+
val nextRad = Math.toRadians(next.toDouble())
|
|
273
|
+
|
|
274
|
+
val sinSmooth = sin(prevRad) + alpha * (sin(nextRad) - sin(prevRad))
|
|
275
|
+
val cosSmooth = cos(prevRad) + alpha * (cos(nextRad) - cos(prevRad))
|
|
276
|
+
|
|
277
|
+
var result = Math.toDegrees(atan2(sinSmooth, cosSmooth)).toFloat()
|
|
278
|
+
if (result < 0) result += 360f
|
|
279
|
+
return result
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Shortest angular distance between two headings (0-180 range).
|
|
284
|
+
*/
|
|
285
|
+
private fun circularDistance(a: Float, b: Float): Float {
|
|
286
|
+
val diff = abs(a - b) % 360f
|
|
287
|
+
return if (diff > 180f) 360f - diff else diff
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fun destroy() {
|
|
291
|
+
stopUpdating()
|
|
292
|
+
onLocationUpdate = null
|
|
293
|
+
onLocationError = null
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
package com.margelo.nitro.nitromap.clustering
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import android.graphics.Typeface
|
|
8
|
+
import android.util.LruCache
|
|
9
|
+
import com.margelo.nitro.nitromap.ClusterConfig
|
|
10
|
+
import com.margelo.nitro.nitromap.markers.PriceMarkerRenderer
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Renders cluster icons as circle + count text.
|
|
14
|
+
* Mirrors iOS ClusterIconRenderer.swift.
|
|
15
|
+
*
|
|
16
|
+
* Uses LruCache to avoid re-rendering the same cluster icon at each zoom level.
|
|
17
|
+
*/
|
|
18
|
+
object ClusterIconRenderer {
|
|
19
|
+
|
|
20
|
+
private const val MAX_CACHE_SIZE = 100
|
|
21
|
+
// L-2: Override entryRemoved to recycle evicted bitmaps and prevent memory leaks
|
|
22
|
+
private val cache = object : LruCache<String, Bitmap>(MAX_CACHE_SIZE) {
|
|
23
|
+
override fun entryRemoved(evicted: Boolean, key: String?, oldValue: Bitmap?, newValue: Bitmap?) {
|
|
24
|
+
if (evicted && oldValue != null && !oldValue.isRecycled) {
|
|
25
|
+
oldValue.recycle()
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Render a cluster icon bitmap.
|
|
32
|
+
*
|
|
33
|
+
* @param count Number of markers in the cluster
|
|
34
|
+
* @param config Optional cluster configuration for colors/border
|
|
35
|
+
* @param density Screen density for sizing
|
|
36
|
+
* @return Rendered cluster icon bitmap
|
|
37
|
+
*/
|
|
38
|
+
fun renderIcon(count: Int, config: ClusterConfig?, density: Float): Bitmap {
|
|
39
|
+
// Check cache
|
|
40
|
+
val cacheKey = buildCacheKey(count, config)
|
|
41
|
+
cache.get(cacheKey)?.let { return it }
|
|
42
|
+
|
|
43
|
+
val diameter = clusterDiameter(count) * density
|
|
44
|
+
val diameterInt = diameter.toInt()
|
|
45
|
+
|
|
46
|
+
val bitmap = Bitmap.createBitmap(diameterInt, diameterInt, Bitmap.Config.ARGB_8888)
|
|
47
|
+
val canvas = Canvas(bitmap)
|
|
48
|
+
|
|
49
|
+
// M-4: Use shared resolveColor from PriceMarkerRenderer
|
|
50
|
+
val bgColor = PriceMarkerRenderer.resolveColor(config?.backgroundColor, Color.argb(255, 0, 122, 255))
|
|
51
|
+
val textColor = PriceMarkerRenderer.resolveColor(config?.textColor, Color.WHITE)
|
|
52
|
+
val borderColor = PriceMarkerRenderer.resolveColor(config?.borderColor, Color.WHITE)
|
|
53
|
+
val borderWidth = ((config?.borderWidth ?: 2.0) * density).toFloat()
|
|
54
|
+
|
|
55
|
+
// Draw border circle
|
|
56
|
+
val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
57
|
+
color = borderColor
|
|
58
|
+
style = Paint.Style.FILL
|
|
59
|
+
}
|
|
60
|
+
canvas.drawCircle(diameter / 2f, diameter / 2f, diameter / 2f, borderPaint)
|
|
61
|
+
|
|
62
|
+
// Draw background circle (inset by border width)
|
|
63
|
+
val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
64
|
+
color = bgColor
|
|
65
|
+
style = Paint.Style.FILL
|
|
66
|
+
}
|
|
67
|
+
canvas.drawCircle(diameter / 2f, diameter / 2f, diameter / 2f - borderWidth, bgPaint)
|
|
68
|
+
|
|
69
|
+
// Draw count text
|
|
70
|
+
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
71
|
+
color = textColor
|
|
72
|
+
textSize = diameter * 0.35f
|
|
73
|
+
typeface = Typeface.DEFAULT_BOLD
|
|
74
|
+
textAlign = Paint.Align.CENTER
|
|
75
|
+
}
|
|
76
|
+
val textBounds = android.graphics.Rect()
|
|
77
|
+
val text = "$count"
|
|
78
|
+
textPaint.getTextBounds(text, 0, text.length, textBounds)
|
|
79
|
+
val textY = diameter / 2f + textBounds.height() / 2f
|
|
80
|
+
canvas.drawText(text, diameter / 2f, textY, textPaint)
|
|
81
|
+
|
|
82
|
+
cache.put(cacheKey, bitmap)
|
|
83
|
+
return bitmap
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Standard cluster diameter in dp based on count.
|
|
88
|
+
* Matches iOS: 44pt for <10, 52pt for <100, 60pt for ≥100.
|
|
89
|
+
*/
|
|
90
|
+
fun clusterDiameter(count: Int): Float {
|
|
91
|
+
return when {
|
|
92
|
+
count < 10 -> 44f
|
|
93
|
+
count < 100 -> 52f
|
|
94
|
+
else -> 60f
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fun clearCache() {
|
|
99
|
+
cache.evictAll()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private fun buildCacheKey(count: Int, config: ClusterConfig?): String {
|
|
103
|
+
val bgHash = config?.backgroundColor?.hashCode() ?: 0
|
|
104
|
+
val textHash = config?.textColor?.hashCode() ?: 0
|
|
105
|
+
val borderHash = config?.borderColor?.hashCode() ?: 0
|
|
106
|
+
val borderW = config?.borderWidth ?: 2.0
|
|
107
|
+
return "cluster_${count}_${bgHash}_${textHash}_${borderHash}_${borderW}"
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// M-4: resolveColor is now shared via PriceMarkerRenderer.resolveColor()
|
|
111
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
package com.margelo.nitro.nitromap.clustering
|
|
2
|
+
|
|
3
|
+
import android.graphics.Bitmap
|
|
4
|
+
import android.os.Handler
|
|
5
|
+
import android.os.Looper
|
|
6
|
+
import com.margelo.nitro.nitromap.ClusterConfig
|
|
7
|
+
import com.margelo.nitro.nitromap.MarkerData
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Clustering manager — wraps NitroClusterEngine and ClusterIconRenderer.
|
|
11
|
+
* Mirrors iOS ClusteringManager.swift.
|
|
12
|
+
*
|
|
13
|
+
* Provides:
|
|
14
|
+
* - Marker management → feeds to C++ engine
|
|
15
|
+
* - Configuration → radius, minSize, maxZoom
|
|
16
|
+
* - Clustering query → bounds-based spatial grouping
|
|
17
|
+
* - Debounce → timer-based to prevent rapid re-clustering
|
|
18
|
+
* - Icon generation → delegates to ClusterIconRenderer
|
|
19
|
+
*/
|
|
20
|
+
class ClusteringManager(
|
|
21
|
+
clusterRadius: Double = 80.0,
|
|
22
|
+
minClusterSize: Int = 2,
|
|
23
|
+
maxZoom: Double = 20.0,
|
|
24
|
+
private val debounceIntervalMs: Long = 100L
|
|
25
|
+
) {
|
|
26
|
+
|
|
27
|
+
private val engine = NitroClusterEngine()
|
|
28
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
29
|
+
private var debounceRunnable: Runnable? = null
|
|
30
|
+
private var density: Float = 1f
|
|
31
|
+
|
|
32
|
+
var clusterConfig: ClusterConfig? = null
|
|
33
|
+
set(value) {
|
|
34
|
+
field = value
|
|
35
|
+
value?.let {
|
|
36
|
+
engine.setClusterRadius(it.radius ?: 80.0)
|
|
37
|
+
engine.setMinClusterSize((it.minimumClusterSize ?: 2.0).toInt())
|
|
38
|
+
engine.setMaxZoom(it.maxZoom ?: 20.0)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
init {
|
|
43
|
+
engine.setClusterRadius(clusterRadius)
|
|
44
|
+
engine.setMinClusterSize(minClusterSize)
|
|
45
|
+
engine.setMaxZoom(maxZoom)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fun setDensity(density: Float) {
|
|
49
|
+
this.density = density
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// MARK: - Marker Management
|
|
53
|
+
|
|
54
|
+
fun addMarker(marker: MarkerData) {
|
|
55
|
+
engine.addMarker(
|
|
56
|
+
marker.id,
|
|
57
|
+
marker.coordinate.latitude,
|
|
58
|
+
marker.coordinate.longitude
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fun removeMarker(id: String) {
|
|
63
|
+
engine.removeMarker(id)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fun clearMarkers() {
|
|
67
|
+
engine.clearMarkers()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// MARK: - Clustering
|
|
71
|
+
|
|
72
|
+
fun cluster(
|
|
73
|
+
minLat: Double, maxLat: Double,
|
|
74
|
+
minLon: Double, maxLon: Double,
|
|
75
|
+
zoom: Double,
|
|
76
|
+
mapWidth: Double, mapHeight: Double
|
|
77
|
+
): NitroClusterEngine.ClusteringResult {
|
|
78
|
+
return engine.clusterWithBounds(minLat, maxLat, minLon, maxLon, zoom, mapWidth, mapHeight)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// MARK: - Debounce
|
|
82
|
+
|
|
83
|
+
fun debounce(action: () -> Unit) {
|
|
84
|
+
debounceRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
85
|
+
val runnable = Runnable { action() }
|
|
86
|
+
debounceRunnable = runnable
|
|
87
|
+
mainHandler.postDelayed(runnable, debounceIntervalMs)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// MARK: - Icon Generation
|
|
91
|
+
|
|
92
|
+
fun clusterIcon(forCount: Int): Bitmap? {
|
|
93
|
+
return ClusterIconRenderer.renderIcon(forCount, clusterConfig, density)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// MARK: - Cleanup
|
|
97
|
+
|
|
98
|
+
fun destroy() {
|
|
99
|
+
debounceRunnable?.let { mainHandler.removeCallbacks(it) }
|
|
100
|
+
debounceRunnable = null
|
|
101
|
+
engine.destroy()
|
|
102
|
+
ClusterIconRenderer.clearCache()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
package com.margelo.nitro.nitromap.clustering
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Kotlin wrapper for C++ ClusterEngine via JNI.
|
|
5
|
+
* Mirrors iOS NitroClusterEngine.swift.
|
|
6
|
+
*
|
|
7
|
+
* Pure spatial grouping — no icon dimensions.
|
|
8
|
+
* Caller (GoogleMapProvider+Clustering) handles visual collision with real rendered sizes.
|
|
9
|
+
*/
|
|
10
|
+
// H-5: Implement AutoCloseable instead of relying on unreliable finalize()
|
|
11
|
+
class NitroClusterEngine : AutoCloseable {
|
|
12
|
+
|
|
13
|
+
// Native pointer to C++ ClusterEngine
|
|
14
|
+
private var nativePtr: Long = nativeCreate()
|
|
15
|
+
|
|
16
|
+
// MARK: - JNI Data Classes
|
|
17
|
+
|
|
18
|
+
/** JNI-friendly cluster data — created in C++ JNI bridge */
|
|
19
|
+
class ClusterDataJNI(
|
|
20
|
+
val latitude: Double,
|
|
21
|
+
val longitude: Double,
|
|
22
|
+
val count: Int,
|
|
23
|
+
val markerIds: Array<String>,
|
|
24
|
+
val iconSize: Double
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
/** JNI-friendly single marker point */
|
|
28
|
+
class SingleMarkerJNI(
|
|
29
|
+
val markerId: String,
|
|
30
|
+
val latitude: Double,
|
|
31
|
+
val longitude: Double
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
/** JNI-friendly clustering result */
|
|
35
|
+
class ClusteringResultJNI(
|
|
36
|
+
val clusters: Array<ClusterDataJNI>,
|
|
37
|
+
val singles: Array<SingleMarkerJNI>
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// MARK: - Public Result Types (clean Kotlin API)
|
|
41
|
+
|
|
42
|
+
data class ClusterDataResult(
|
|
43
|
+
val latitude: Double,
|
|
44
|
+
val longitude: Double,
|
|
45
|
+
val markerIds: List<String>,
|
|
46
|
+
val count: Int,
|
|
47
|
+
val iconSize: Double
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
data class SingleMarkerResult(
|
|
51
|
+
val markerId: String,
|
|
52
|
+
val latitude: Double,
|
|
53
|
+
val longitude: Double
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
data class ClusteringResult(
|
|
57
|
+
val clusters: List<ClusterDataResult>,
|
|
58
|
+
val singleMarkers: List<SingleMarkerResult>
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
// MARK: - Configuration
|
|
62
|
+
// M-6: All public methods guard against use-after-destroy (nativePtr == 0L)
|
|
63
|
+
|
|
64
|
+
fun setClusterRadius(radius: Double) {
|
|
65
|
+
if (nativePtr == 0L) return
|
|
66
|
+
nativeSetClusterRadius(nativePtr, radius)
|
|
67
|
+
}
|
|
68
|
+
fun setMinClusterSize(size: Int) {
|
|
69
|
+
if (nativePtr == 0L) return
|
|
70
|
+
nativeSetMinClusterSize(nativePtr, size)
|
|
71
|
+
}
|
|
72
|
+
fun setMaxZoom(zoom: Double) {
|
|
73
|
+
if (nativePtr == 0L) return
|
|
74
|
+
nativeSetMaxZoom(nativePtr, zoom)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MARK: - Marker Management
|
|
78
|
+
|
|
79
|
+
fun addMarker(id: String, latitude: Double, longitude: Double) {
|
|
80
|
+
if (nativePtr == 0L) return
|
|
81
|
+
nativeAddMarker(nativePtr, id, latitude, longitude)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fun removeMarker(id: String) {
|
|
85
|
+
if (nativePtr == 0L) return
|
|
86
|
+
nativeRemoveMarker(nativePtr, id)
|
|
87
|
+
}
|
|
88
|
+
fun clearMarkers() {
|
|
89
|
+
if (nativePtr == 0L) return
|
|
90
|
+
nativeClearMarkers(nativePtr)
|
|
91
|
+
}
|
|
92
|
+
fun markerCount(): Int {
|
|
93
|
+
if (nativePtr == 0L) return 0
|
|
94
|
+
return nativeMarkerCount(nativePtr)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// MARK: - Clustering
|
|
98
|
+
|
|
99
|
+
fun clusterWithBounds(
|
|
100
|
+
minLat: Double, maxLat: Double,
|
|
101
|
+
minLon: Double, maxLon: Double,
|
|
102
|
+
zoom: Double,
|
|
103
|
+
mapWidth: Double, mapHeight: Double
|
|
104
|
+
): ClusteringResult {
|
|
105
|
+
if (nativePtr == 0L) return ClusteringResult(emptyList(), emptyList())
|
|
106
|
+
val jniResults = nativeCluster(nativePtr, minLat, maxLat, minLon, maxLon, zoom, mapWidth, mapHeight)
|
|
107
|
+
|
|
108
|
+
if (jniResults.isEmpty()) {
|
|
109
|
+
return ClusteringResult(emptyList(), emptyList())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
val jniResult = jniResults[0]
|
|
113
|
+
|
|
114
|
+
val clusters = jniResult.clusters.map { c ->
|
|
115
|
+
ClusterDataResult(
|
|
116
|
+
latitude = c.latitude,
|
|
117
|
+
longitude = c.longitude,
|
|
118
|
+
markerIds = c.markerIds.toList(),
|
|
119
|
+
count = c.count,
|
|
120
|
+
iconSize = c.iconSize
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
val singles = jniResult.singles.map { s ->
|
|
125
|
+
SingleMarkerResult(
|
|
126
|
+
markerId = s.markerId,
|
|
127
|
+
latitude = s.latitude,
|
|
128
|
+
longitude = s.longitude
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return ClusteringResult(clusters, singles)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// MARK: - Cleanup
|
|
136
|
+
|
|
137
|
+
fun destroy() {
|
|
138
|
+
if (nativePtr != 0L) {
|
|
139
|
+
nativeDestroy(nativePtr)
|
|
140
|
+
nativePtr = 0
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// H-5: AutoCloseable.close() — deterministic cleanup via try-with-resources or explicit close
|
|
145
|
+
override fun close() {
|
|
146
|
+
destroy()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// MARK: - JNI External Functions
|
|
150
|
+
|
|
151
|
+
private external fun nativeCreate(): Long
|
|
152
|
+
private external fun nativeDestroy(ptr: Long)
|
|
153
|
+
private external fun nativeSetClusterRadius(ptr: Long, radius: Double)
|
|
154
|
+
private external fun nativeSetMinClusterSize(ptr: Long, size: Int)
|
|
155
|
+
private external fun nativeSetMaxZoom(ptr: Long, zoom: Double)
|
|
156
|
+
private external fun nativeAddMarker(ptr: Long, id: String, lat: Double, lon: Double)
|
|
157
|
+
private external fun nativeRemoveMarker(ptr: Long, id: String)
|
|
158
|
+
private external fun nativeClearMarkers(ptr: Long)
|
|
159
|
+
private external fun nativeMarkerCount(ptr: Long): Int
|
|
160
|
+
private external fun nativeCluster(
|
|
161
|
+
ptr: Long,
|
|
162
|
+
minLat: Double, maxLat: Double,
|
|
163
|
+
minLon: Double, maxLon: Double,
|
|
164
|
+
zoom: Double, mapW: Double, mapH: Double
|
|
165
|
+
): Array<ClusteringResultJNI>
|
|
166
|
+
}
|