diff --git a/.history/README_20250614152201.md b/.history/README_20250614152201.md new file mode 100644 index 0000000..826b1d1 --- /dev/null +++ b/.history/README_20250614152201.md @@ -0,0 +1,129 @@ +# dumon-geolocation + +Implement manager GNSS, Wiโ€‘Fi RTT, IMU, Kalman fusion, event emitter + +## Install + +```bash +npm install dumon-geolocation +npx cap sync +``` + +## API + + + +* [`startPositioning()`](#startpositioning) +* [`stopPositioning()`](#stoppositioning) +* [`getLatestPosition()`](#getlatestposition) +* [`addListener('onPositionUpdate', ...)`](#addlisteneronpositionupdate-) +* [Interfaces](#interfaces) + + + + + + +### startPositioning() + +```typescript +startPositioning() => Promise +``` + +-------------------- + + +### stopPositioning() + +```typescript +stopPositioning() => Promise +``` + +-------------------- + + +### getLatestPosition() + +```typescript +getLatestPosition() => Promise +``` + +**Returns:** Promise<PositioningData> + +-------------------- + + +### addListener('onPositionUpdate', ...) + +```typescript +addListener(eventName: 'onPositionUpdate', listenerFunc: (data: PositioningData) => void) => PluginListenerHandle +``` + +| Param | Type | +| ------------------ | ------------------------------------------------------------------------------ | +| **`eventName`** | 'onPositionUpdate' | +| **`listenerFunc`** | (data: PositioningData) => void | + +**Returns:** PluginListenerHandle + +-------------------- + + +### Interfaces + + +#### PositioningData + +| Prop | Type | +| --------------- | ----------------------------------------------------------- | +| **`source`** | 'GNSS' \| 'WIFI' \| 'FUSED' \| 'MOCK' | +| **`timestamp`** | number | +| **`latitude`** | number | +| **`longitude`** | number | +| **`accuracy`** | number | +| **`gnssData`** | SatelliteStatus | +| **`wifiData`** | WifiAp[] | +| **`imuData`** | ImuData | + + +#### SatelliteStatus + +| Prop | Type | +| ------------------------- | --------------------------------------- | +| **`satellitesInView`** | number | +| **`usedInFix`** | number | +| **`constellationCounts`** | { [key: string]: number; } | + + +#### WifiAp + +| Prop | Type | +| -------------- | ------------------- | +| **`ssid`** | string | +| **`bssid`** | string | +| **`rssi`** | number | +| **`distance`** | number | + + +#### ImuData + +| Prop | Type | +| ------------------ | ------------------- | +| **`accelX`** | number | +| **`accelY`** | number | +| **`accelZ`** | number | +| **`gyroX`** | number | +| **`gyroY`** | number | +| **`gyroZ`** | number | +| **`speed`** | number | +| **`acceleration`** | number | +| **`directionRad`** | number | + + +#### PluginListenerHandle + +| Prop | Type | +| ------------ | ----------------------------------------- | +| **`remove`** | () => Promise<void> | + + diff --git a/.history/README_20250614152812.md b/.history/README_20250614152812.md new file mode 100644 index 0000000..e29475c --- /dev/null +++ b/.history/README_20250614152812.md @@ -0,0 +1,133 @@ +# dumon-geolocation + +Plugin Capacitor Android untuk positioning real-time berbasis GNSS multi-konstelasi, Wi-Fi RTT/RSSI, dan IMU (Accelerometer + Gyroscope), dilengkapi dengan sensor fusion (Kalman Filter) dan deteksi lokasi palsu. + +## ๐Ÿ“ฆ Install + +```bash +npm install dumon-geolocation +npx cap sync +``` + +## ๐Ÿš€ API + +### ๐Ÿ“ก startPositioning() + +```ts +startPositioning() => Promise +``` + +Memulai pengambilan data posisi secara real-time dari GNSS, Wi-Fi, dan IMU. + +--- + +### ๐Ÿ›‘ stopPositioning() + +```ts +stopPositioning() => Promise +``` + +Menghentikan semua sensor dan positioning. + +--- + +### ๐Ÿ“ getLatestPosition() + +```ts +getLatestPosition() => Promise +``` + +Mengembalikan data posisi terkini yang telah difusi. + +--- + +### ๐Ÿ”„ addListener('onPositionUpdate', ...) + +```ts +addListener(eventName: 'onPositionUpdate', listenerFunc: (data: PositioningData) => void): PluginListenerHandle +``` + +Listener untuk update posisi secara berkala (real-time). + +--- + +## ๐Ÿงพ Interfaces + +### PositioningData + +```ts +interface PositioningData { + source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK'; + latitude: number; + longitude: number; + accuracy: number; + speed: number; + acceleration: number; + directionRad: number; + timestamp: number; + isMocked: boolean; + + // Optional raw sensor data (available internally) + // imuData?: ImuData; + // gnssData?: SatelliteStatus; + // wifiData?: WifiAp[]; +} +``` + +### ImuData + +```ts +interface ImuData { + accelX: number; + accelY: number; + accelZ: number; + gyroX: number; + gyroY: number; + gyroZ: number; + speed: number; + acceleration: number; + directionRad: number; +} +``` + +### SatelliteStatus + +```ts +interface SatelliteStatus { + satellitesInView: number; + usedInFix: number; + constellationCounts: { [key: string]: number }; +} +``` + +### WifiAp + +```ts +interface WifiAp { + ssid: string; + bssid: string; + rssi: number; + distance?: number; +} +``` + +### PluginListenerHandle + +```ts +interface PluginListenerHandle { + remove: () => Promise; +} +``` + +--- + +## โ„น๏ธ Catatan +- Plugin hanya mendukung platform Android saat ini. +- Ideal digunakan bersama dengan plugin `Geolocation` bawaan Capacitor untuk fallback atau perbandingan. +- Sensor fusion berbasis Kalman Filter (versi sederhana). +- `directionRad` merujuk arah dalam radian relatif terhadap utara (azimuth). +- Output `isMocked` berguna untuk deteksi lokasi palsu. + +--- + +Lisensi: MIT โ€“ Dibuat oleh Tim Dumon \ No newline at end of file diff --git a/.history/android/build_20250614102811.gradle b/.history/android/build_20250614102811.gradle new file mode 100644 index 0000000..33ad8bc --- /dev/null +++ b/.history/android/build_20250614102811.gradle @@ -0,0 +1,67 @@ +ext { + junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' + androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0' + androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1' + androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1' +} + +buildscript { + ext { + kotlin_version = '1.9.24' + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.7.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' + +android { + namespace "com.dumon.plugin.geolocation" + compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35 + defaultConfig { + minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23 + targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } + kotlinOptions { + jvmTarget = '21' + } +} + +repositories { + google() + mavenCentral() +} + + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':capacitor-android') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation 'androidx.core:core-ktx:1.16.0' + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" +} diff --git a/.history/android/build_20250614102909.gradle b/.history/android/build_20250614102909.gradle new file mode 100644 index 0000000..637e149 --- /dev/null +++ b/.history/android/build_20250614102909.gradle @@ -0,0 +1,67 @@ +ext { + junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' + androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0' + androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1' + androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1' +} + +buildscript { + ext { + kotlin_version = '1.9.24' + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.7.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' + +android { + namespace "com.dumon.plugin.geolocation" + compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35 + defaultConfig { + minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23 + targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } +} + +repositories { + google() + mavenCentral() +} + + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':capacitor-android') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation 'androidx.core:core-ktx:1.16.0' + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" +} diff --git a/.history/android/build_20250614115958.gradle b/.history/android/build_20250614115958.gradle new file mode 100644 index 0000000..9940916 --- /dev/null +++ b/.history/android/build_20250614115958.gradle @@ -0,0 +1,67 @@ +ext { + junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' + androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0' + androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1' + androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1' +} + +buildscript { + ext { + kotlin_version = '1.9.24' + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.7.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' + +android { + namespace "com.dumon.plugin.geolocation" + compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35 + defaultConfig { + minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24 + targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } +} + +repositories { + google() + mavenCentral() +} + + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(':capacitor-android') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation 'androidx.core:core-ktx:1.16.0' + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" +} diff --git a/.history/android/src/main/AndroidManifest_20240404000303.xml b/.history/android/src/main/AndroidManifest_20240404000303.xml new file mode 100644 index 0000000..a2f47b6 --- /dev/null +++ b/.history/android/src/main/AndroidManifest_20240404000303.xml @@ -0,0 +1,2 @@ + + diff --git a/.history/android/src/main/AndroidManifest_20250614102035.xml b/.history/android/src/main/AndroidManifest_20250614102035.xml new file mode 100644 index 0000000..ef0d267 --- /dev/null +++ b/.history/android/src/main/AndroidManifest_20250614102035.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.java b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614100016.kt similarity index 100% rename from android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.java rename to .history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614100016.kt diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614102719.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614102719.kt new file mode 100644 index 0000000..12f422f --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614102719.kt @@ -0,0 +1,158 @@ +package com.dumon.plugin.geolocation; + +import android.util.Log; +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.fusion.SensorFusionManager + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var sensorFusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { info -> + // Optional: You can choose to emit this as another event + // notifyListeners("onGpsStatus", JSObject().put("info", info)) + }, + onLocationUpdate = { info -> + // Example parsing GPS location (assume GNSS is main source) + // For now, dummy parser โ†’ in real code you can extract values properly + latestLatitude += 0.0001 + latestLongitude += 0.0001 + latestAccuracy = 5.0 + latestSource = "GNSS" + + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { info -> + // You can parse accel if needed + // Example dummy update: + latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) + }, + onGyroscopeUpdate = { info -> + // Example dummy update: + latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { info -> + // Optional: could update fused position here + } + ) + + sensorFusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + wifiManager?.startPeriodicScan(3000L) + imuManager?.start() + + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + wifiManager?.stopPeriodicScan() + imuManager?.stop() + + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + call.resolve(jsObj) + } + + // Helper โ†’ Emit to JS + private fun emitPositionUpdate() { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + notifyListeners("onPositionUpdate", jsObj) + } +} + +// import android.util.Log; + +// public class DumonGeolocation { + +// public String echo(String value) { +// Log.i("Echo", value); +// return value; +// } +// } diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614111702.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614111702.kt new file mode 100644 index 0000000..d03e186 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614111702.kt @@ -0,0 +1,155 @@ +package com.dumon.plugin.geolocation; + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.fusion.SensorFusionManager + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var sensorFusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { info -> + // Optional: You can choose to emit this as another event + // notifyListeners("onGpsStatus", JSObject().put("info", info)) + }, + onLocationUpdate = { info -> + // Example parsing GPS location (assume GNSS is main source) + // For now, dummy parser โ†’ in real code you can extract values properly + // latestLatitude += 0.0001 + // latestLongitude += 0.0001 + // latestAccuracy = 5.0 + // latestSource = "GNSS" + + + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = "GNSS" + + emitPositionUpdate() + + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { info -> + // You can parse accel if needed + // Example dummy update: + latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) + }, + onGyroscopeUpdate = { info -> + // Example dummy update: + latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { info -> + // Optional: could update fused position here + } + ) + + sensorFusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + wifiManager?.startPeriodicScan(3000L) + imuManager?.start() + + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + wifiManager?.stopPeriodicScan() + imuManager?.stop() + + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + call.resolve(jsObj) + } + + // Helper โ†’ Emit to JS + private fun emitPositionUpdate() { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + notifyListeners("onPositionUpdate", jsObj) + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614111727.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614111727.kt new file mode 100644 index 0000000..ad258c3 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614111727.kt @@ -0,0 +1,146 @@ +package com.dumon.plugin.geolocation; + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.fusion.SensorFusionManager + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var sensorFusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { info -> + // Optional: You can choose to emit this as another event + // notifyListeners("onGpsStatus", JSObject().put("info", info)) + }, + onLocationUpdate = { location -> + + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = "GNSS" + + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { info -> + // You can parse accel if needed + // Example dummy update: + latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) + }, + onGyroscopeUpdate = { info -> + // Example dummy update: + latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { info -> + // Optional: could update fused position here + } + ) + + sensorFusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + wifiManager?.startPeriodicScan(3000L) + imuManager?.start() + + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + wifiManager?.stopPeriodicScan() + imuManager?.stop() + + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + call.resolve(jsObj) + } + + // Helper โ†’ Emit to JS + private fun emitPositionUpdate() { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + notifyListeners("onPositionUpdate", jsObj) + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614112034.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614112034.kt new file mode 100644 index 0000000..432b618 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614112034.kt @@ -0,0 +1,148 @@ +package com.dumon.plugin.geolocation; + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.fusion.SensorFusionManager + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var sensorFusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { info -> + // Optional: You can choose to emit this as another event + // notifyListeners("onGpsStatus", JSObject().put("info", info)) + }, + onLocationUpdate = { location -> + + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = "GNSS" + latestTimestamp = location.time + + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { info -> + // You can parse accel if needed + // Example dummy update: + latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) + }, + onGyroscopeUpdate = { info -> + // Example dummy update: + latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { info -> + // Optional: could update fused position here + } + ) + + sensorFusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + wifiManager?.startPeriodicScan(3000L) + imuManager?.start() + + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + wifiManager?.stopPeriodicScan() + imuManager?.stop() + + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + call.resolve(jsObj) + } + + // Helper โ†’ Emit to JS + private fun emitPositionUpdate() { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + notifyListeners("onPositionUpdate", jsObj) + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614112109.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614112109.kt new file mode 100644 index 0000000..a7271b3 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614112109.kt @@ -0,0 +1,150 @@ +package com.dumon.plugin.geolocation; + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.fusion.SensorFusionManager + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var sensorFusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { info -> + // Optional: You can choose to emit this as another event + // notifyListeners("onGpsStatus", JSObject().put("info", info)) + }, + onLocationUpdate = { location -> + + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = "GNSS" + latestTimestamp = location.time + + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { info -> + // You can parse accel if needed + // Example dummy update: + latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) + }, + onGyroscopeUpdate = { info -> + // Example dummy update: + latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { info -> + // Optional: could update fused position here + } + ) + + sensorFusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + wifiManager?.startPeriodicScan(3000L) + imuManager?.start() + + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + wifiManager?.stopPeriodicScan() + imuManager?.stop() + + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + jsObj.put("timestamp", latestTimestamp) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + call.resolve(jsObj) + } + + // Helper โ†’ Emit to JS + private fun emitPositionUpdate() { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + jsObj.put("timestamp", latestTimestamp) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + notifyListeners("onPositionUpdate", jsObj) + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614113526.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614113526.kt new file mode 100644 index 0000000..d37164e --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614113526.kt @@ -0,0 +1,183 @@ +package com.dumon.plugin.geolocation; + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var sensorFusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + private var latestSatelliteStatus: SatelliteStatus? = null + private var latestWifiScanResult: WifiScanResult? = null + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { status -> + latestSatelliteStatus = status + }, + onLocationUpdate = { location -> + + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = "GNSS" + latestTimestamp = location.time + + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { info -> + // You can parse accel if needed + // Example dummy update: + latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) + }, + onGyroscopeUpdate = { info -> + // Example dummy update: + latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { result -> + latestWifiScanResult = result + } + ) + + sensorFusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + wifiManager?.startPeriodicScan(3000L) + imuManager?.start() + + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + wifiManager?.stopPeriodicScan() + imuManager?.stop() + + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + jsObj.put("timestamp", latestTimestamp) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + call.resolve(jsObj) + } + + // Helper โ†’ Emit to JS + private fun emitPositionUpdate() { + val jsObj = JSObject() + jsObj.put("source", latestSource) + jsObj.put("timestamp", latestTimestamp) + + val gpsData = JSObject() + gpsData.put("latitude", latestLatitude) + gpsData.put("longitude", latestLongitude) + gpsData.put("accuracy", latestAccuracy) + + latestSatelliteStatus?.let { status -> + gpsData.put("satellitesInView", status.satellitesInView) + gpsData.put("usedInFix", status.usedInFix) + val constellations = JSObject() + status.constellationCounts.forEach { (name, count) -> + constellations.put(name, count) + } + gpsData.put("constellationCounts", constellations) + } + + jsObj.put("gpsData", gpsData) + + val wifiData = JSObject() + latestWifiScanResult?.let { result -> + wifiData.put("apCount", result.apCount) + val apsArray = JSArray() + result.aps.forEach { ap -> + val apObj = JSObject() + apObj.put("ssid", ap.ssid) + apObj.put("bssid", ap.bssid) + apObj.put("rssi", ap.rssi) + ap.distance?.let { d -> apObj.put("distance", d) } + apsArray.put(apObj) + } + wifiData.put("aps", apsArray) + } + jsObj.put("wifiData", wifiData) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + jsObj.put("imuData", imuData) + + notifyListeners("onPositionUpdate", jsObj) + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614114204.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614114204.kt new file mode 100644 index 0000000..4a4b8df --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614114204.kt @@ -0,0 +1,183 @@ +package com.dumon.plugin.geolocation; + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var sensorFusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + private var latestSatelliteStatus: SatelliteStatus? = null + private var latestWifiScanResult: WifiScanResult? = null + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { status -> + latestSatelliteStatus = status + }, + onLocationUpdate = { location -> + + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = "GNSS" + latestTimestamp = location.time + + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { info -> + // You can parse accel if needed + // Example dummy update: + latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) + }, + onGyroscopeUpdate = { info -> + // Example dummy update: + latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { result -> + latestWifiScanResult = result + } + ) + + sensorFusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + wifiManager?.startPeriodicScan(3000L) + imuManager?.start() + + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + wifiManager?.stopPeriodicScan() + imuManager?.stop() + + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + val jsObj = JSObject() + jsObj.put("latitude", latestLatitude) + jsObj.put("longitude", latestLongitude) + jsObj.put("accuracy", latestAccuracy) + jsObj.put("source", latestSource) + jsObj.put("timestamp", latestTimestamp) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + + jsObj.put("imuData", imuData) + + call.resolve(jsObj) + } + + // Helper โ†’ Emit to JS + private fun emitPositionUpdate() { + val jsObj = JSObject() + jsObj.put("source", latestSource) + jsObj.put("timestamp", latestTimestamp) + + val gpsData = JSObject() + gpsData.put("latitude", latestLatitude) + gpsData.put("longitude", latestLongitude) + gpsData.put("accuracy", latestAccuracy) + + latestSatelliteStatus?.let { status -> + gpsData.put("satellitesInView", status.satellitesInView) + gpsData.put("usedInFix", status.usedInFix) + val constellations = JSObject() + status.constellationCounts.forEach { (name, count) -> + constellations.put(name, count) + } + gpsData.put("constellationCounts", constellations) + } + + jsObj.put("gpsData", gpsData) + + val wifiData = JSObject() + latestWifiScanResult?.let { result -> + wifiData.put("apCount", result.apCount) + val apsArray = JSArray() + result.aps.forEach { ap -> + val apObj = JSObject() + apObj.put("ssid", ap.ssid) + apObj.put("bssid", ap.bssid) + apObj.put("rssi", ap.rssi) + ap.distance?.let { d -> apObj.put("distance", d) } + apsArray.put(apObj) + } + wifiData.put("aps", apsArray) + } + jsObj.put("wifiData", wifiData) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + jsObj.put("imuData", imuData) + + notifyListeners("onPositionUpdate", jsObj.put("timestamp", latestTimestamp.takeIf { it > 0 } ?: System.currentTimeMillis())) + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614114322.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614114322.kt new file mode 100644 index 0000000..908b24c --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614114322.kt @@ -0,0 +1,211 @@ +package com.dumon.plugin.geolocation; + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var sensorFusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + private var latestSatelliteStatus: SatelliteStatus? = null + private var latestWifiScanResult: WifiScanResult? = null + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { status -> + latestSatelliteStatus = status + }, + onLocationUpdate = { location -> + + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = "GNSS" + latestTimestamp = location.time + + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { info -> + // You can parse accel if needed + // Example dummy update: + latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) + }, + onGyroscopeUpdate = { info -> + // Example dummy update: + latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { result -> + latestWifiScanResult = result + } + ) + + sensorFusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + wifiManager?.startPeriodicScan(3000L) + imuManager?.start() + + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + wifiManager?.stopPeriodicScan() + imuManager?.stop() + + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + val jsObj = JSObject() + jsObj.put("source", latestSource) + jsObj.put("timestamp", latestTimestamp) + + val gpsData = JSObject() + gpsData.put("latitude", latestLatitude) + gpsData.put("longitude", latestLongitude) + gpsData.put("accuracy", latestAccuracy) + + latestSatelliteStatus?.let { status -> + gpsData.put("satellitesInView", status.satellitesInView) + gpsData.put("usedInFix", status.usedInFix) + val constellations = JSObject() + status.constellationCounts.forEach { (name, count) -> + constellations.put(name, count) + } + gpsData.put("constellationCounts", constellations) + } + jsObj.put("gpsData", gpsData) + + val wifiData = JSObject() + latestWifiScanResult?.let { result -> + wifiData.put("apCount", result.apCount) + val apsArray = JSArray() + result.aps.forEach { ap -> + val apObj = JSObject() + apObj.put("ssid", ap.ssid) + apObj.put("bssid", ap.bssid) + apObj.put("rssi", ap.rssi) + ap.distance?.let { d -> apObj.put("distance", d) } + apsArray.put(apObj) + } + wifiData.put("aps", apsArray) + } + jsObj.put("wifiData", wifiData) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + jsObj.put("imuData", imuData) + + call.resolve(jsObj) + } + + // Helper โ†’ Emit to JS + private fun emitPositionUpdate() { + val jsObj = JSObject() + jsObj.put("source", latestSource) + jsObj.put("timestamp", latestTimestamp) + + val gpsData = JSObject() + gpsData.put("latitude", latestLatitude) + gpsData.put("longitude", latestLongitude) + gpsData.put("accuracy", latestAccuracy) + + latestSatelliteStatus?.let { status -> + gpsData.put("satellitesInView", status.satellitesInView) + gpsData.put("usedInFix", status.usedInFix) + val constellations = JSObject() + status.constellationCounts.forEach { (name, count) -> + constellations.put(name, count) + } + gpsData.put("constellationCounts", constellations) + } + + jsObj.put("gpsData", gpsData) + + val wifiData = JSObject() + latestWifiScanResult?.let { result -> + wifiData.put("apCount", result.apCount) + val apsArray = JSArray() + result.aps.forEach { ap -> + val apObj = JSObject() + apObj.put("ssid", ap.ssid) + apObj.put("bssid", ap.bssid) + apObj.put("rssi", ap.rssi) + ap.distance?.let { d -> apObj.put("distance", d) } + apsArray.put(apObj) + } + wifiData.put("aps", apsArray) + } + jsObj.put("wifiData", wifiData) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + jsObj.put("imuData", imuData) + + notifyListeners("onPositionUpdate", jsObj.put("timestamp", latestTimestamp.takeIf { it > 0 } ?: System.currentTimeMillis())) + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614114437.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614114437.kt new file mode 100644 index 0000000..c54113b --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614114437.kt @@ -0,0 +1,201 @@ +package com.dumon.plugin.geolocation; + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var sensorFusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + private var latestSatelliteStatus: SatelliteStatus? = null + private var latestWifiScanResult: WifiScanResult? = null + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { status -> + latestSatelliteStatus = status + }, + onLocationUpdate = { location -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = "GNSS" + latestTimestamp = location.time + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { info -> + latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) + }, + onGyroscopeUpdate = { info -> + latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { result -> + latestWifiScanResult = result + } + ) + + sensorFusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + wifiManager?.startPeriodicScan(3000L) + imuManager?.start() + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + wifiManager?.stopPeriodicScan() + imuManager?.stop() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + val jsObj = JSObject() + jsObj.put("source", latestSource) + jsObj.put("timestamp", latestTimestamp.takeIf { it > 0 } ?: System.currentTimeMillis()) + + val gpsData = JSObject() + gpsData.put("latitude", latestLatitude) + gpsData.put("longitude", latestLongitude) + gpsData.put("accuracy", latestAccuracy) + + latestSatelliteStatus?.let { status -> + gpsData.put("satellitesInView", status.satellitesInView) + gpsData.put("usedInFix", status.usedInFix) + val constellations = JSObject() + status.constellationCounts.forEach { (name, count) -> + constellations.put(name, count) + } + gpsData.put("constellationCounts", constellations) + } + jsObj.put("gpsData", gpsData) + + val wifiData = JSObject() + latestWifiScanResult?.let { result -> + wifiData.put("apCount", result.apCount) + val apsArray = JSArray() + result.aps.forEach { ap -> + val apObj = JSObject() + apObj.put("ssid", ap.ssid) + apObj.put("bssid", ap.bssid) + apObj.put("rssi", ap.rssi) + ap.distance?.let { d -> apObj.put("distance", d) } + apsArray.put(apObj) + } + wifiData.put("aps", apsArray) + } + jsObj.put("wifiData", wifiData) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + jsObj.put("imuData", imuData) + + call.resolve(jsObj) + } + + private fun emitPositionUpdate() { + val jsObj = JSObject() + jsObj.put("source", latestSource) + jsObj.put("timestamp", latestTimestamp.takeIf { it > 0 } ?: System.currentTimeMillis()) + + val gpsData = JSObject() + gpsData.put("latitude", latestLatitude) + gpsData.put("longitude", latestLongitude) + gpsData.put("accuracy", latestAccuracy) + + latestSatelliteStatus?.let { status -> + gpsData.put("satellitesInView", status.satellitesInView) + gpsData.put("usedInFix", status.usedInFix) + val constellations = JSObject() + status.constellationCounts.forEach { (name, count) -> + constellations.put(name, count) + } + gpsData.put("constellationCounts", constellations) + } + jsObj.put("gpsData", gpsData) + + val wifiData = JSObject() + latestWifiScanResult?.let { result -> + wifiData.put("apCount", result.apCount) + val apsArray = JSArray() + result.aps.forEach { ap -> + val apObj = JSObject() + apObj.put("ssid", ap.ssid) + apObj.put("bssid", ap.bssid) + apObj.put("rssi", ap.rssi) + ap.distance?.let { d -> apObj.put("distance", d) } + apsArray.put(apObj) + } + wifiData.put("aps", apsArray) + } + jsObj.put("wifiData", wifiData) + + val imuData = JSObject() + imuData.put("accelX", latestAccel[0]) + imuData.put("accelY", latestAccel[1]) + imuData.put("accelZ", latestAccel[2]) + imuData.put("gyroX", latestGyro[0]) + imuData.put("gyroY", latestGyro[1]) + imuData.put("gyroZ", latestGyro[2]) + jsObj.put("imuData", imuData) + + notifyListeners("onPositionUpdate", jsObj) + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614115319.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614115319.kt new file mode 100644 index 0000000..65fe9d6 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614115319.kt @@ -0,0 +1,160 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { + latestLatitude = it.latitude + latestLongitude = it.longitude + latestAccuracy = it.accuracy.toDouble() + latestSource = "GNSS" + latestTimestamp = it.time + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) }, + onGyroscopeUpdate = { latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { wifiScanResult = it } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", latestTimestamp.takeIf { it > 0 } ?: System.currentTimeMillis()) + + // GPS block + val gps = JSObject().apply { + put("latitude", latestLatitude) + put("longitude", latestLongitude) + put("accuracy", latestAccuracy) + } + + satelliteStatus?.let { + gps.put("satellitesInView", it.satellitesInView) + gps.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gps.put("constellationCounts", constellations) + } + + // Wi-Fi block + val wifi = JSObject().apply { + wifiScanResult?.let { + put("apCount", it.apCount) + val aps = JSArray() + it.aps.forEach { ap -> + val a = JSObject().apply { + put("ssid", ap.ssid) + put("bssid", ap.bssid) + put("rssi", ap.rssi) + ap.distance?.let { d -> put("distance", d) } + } + aps.put(a) + } + put("aps", aps) + } + } + + // IMU block + val imu = JSObject().apply { + put("accelX", latestAccel[0]) + put("accelY", latestAccel[1]) + put("accelZ", latestAccel[2]) + put("gyroX", latestGyro[0]) + put("gyroY", latestGyro[1]) + put("gyroZ", latestGyro[2]) + } + + obj.put("gpsData", gps) + obj.put("wifiData", wifi) + obj.put("imuData", imu) + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614123340.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614123340.kt new file mode 100644 index 0000000..557819f --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614123340.kt @@ -0,0 +1,160 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestAccel = floatArrayOf(0f, 0f, 0f) + private var latestGyro = floatArrayOf(0f, 0f, 0f) + + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { location, isMocked -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = if (isMocked) "MOCK" else "GNSS" + latestTimestamp = location.time + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onAccelerometerUpdate = { latestAccel = floatArrayOf(0.1f, 0.2f, 0.3f) }, + onGyroscopeUpdate = { latestGyro = floatArrayOf(0.01f, 0.02f, 0.03f) } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { wifiScanResult = it } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", latestTimestamp.takeIf { it > 0 } ?: System.currentTimeMillis()) + + // GPS block + val gps = JSObject().apply { + put("latitude", latestLatitude) + put("longitude", latestLongitude) + put("accuracy", latestAccuracy) + } + + satelliteStatus?.let { + gps.put("satellitesInView", it.satellitesInView) + gps.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gps.put("constellationCounts", constellations) + } + + // Wi-Fi block + val wifi = JSObject().apply { + wifiScanResult?.let { + put("apCount", it.apCount) + val aps = JSArray() + it.aps.forEach { ap -> + val a = JSObject().apply { + put("ssid", ap.ssid) + put("bssid", ap.bssid) + put("rssi", ap.rssi) + ap.distance?.let { d -> put("distance", d) } + } + aps.put(a) + } + put("aps", aps) + } + } + + // IMU block + val imu = JSObject().apply { + put("accelX", latestAccel[0]) + put("accelY", latestAccel[1]) + put("accelZ", latestAccel[2]) + put("gyroX", latestGyro[0]) + put("gyroY", latestGyro[1]) + put("gyroZ", latestGyro[2]) + } + + obj.put("gpsData", gps) + obj.put("wifiData", wifi) + obj.put("imuData", imu) + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614135559.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614135559.kt new file mode 100644 index 0000000..f919564 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614135559.kt @@ -0,0 +1,156 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuData +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestImu: ImuData? = null + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { location, isMocked -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = if (isMocked) "MOCK" else "GNSS" + latestTimestamp = location.time + fusionManager?.updateGpsPosition(latestLatitude, latestLongitude) + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onImuUpdate = { latestImu = it } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { wifiScanResult = it } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) + obj.put("latitude", latestLatitude) + obj.put("longitude", latestLongitude) + obj.put("accuracy", latestAccuracy) + + // GPS metadata + satelliteStatus?.let { + val gnss = JSObject() + gnss.put("satellitesInView", it.satellitesInView) + gnss.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gnss.put("constellationCounts", constellations) + obj.put("gnssData", gnss) + } + + // Wi-Fi metadata + wifiScanResult?.let { + val wifi = JSArray() + it.aps.forEach { ap -> + val a = JSObject() + a.put("ssid", ap.ssid) + a.put("bssid", ap.bssid) + a.put("rssi", ap.rssi) + ap.distance?.let { d -> a.put("distance", d) } + wifi.put(a) + } + obj.put("wifiData", wifi) + } + + // IMU metadata + latestImu?.let { + val imu = JSObject() + imu.put("accelX", it.accelX) + imu.put("accelY", it.accelY) + imu.put("accelZ", it.accelZ) + imu.put("gyroX", it.gyroX) + imu.put("gyroY", it.gyroY) + imu.put("gyroZ", it.gyroZ) + imu.put("speed", it.speed) + imu.put("acceleration", it.acceleration) + imu.put("directionRad", it.directionRad) + obj.put("imuData", imu) + } + + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614141010.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614141010.kt new file mode 100644 index 0000000..a66ff51 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614141010.kt @@ -0,0 +1,160 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuData +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestImu: ImuData? = null + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + private var isMockedLocation = false + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { location, isMocked -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = if (isMocked) "MOCK" else "GNSS" + isMockedLocation = isMocked // sudah tersedia dari GpsStatusManager + latestTimestamp = location.time + fusionManager?.updateGpsPosition(latestLatitude, latestLongitude) + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onImuUpdate = { latestImu = it } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { wifiScanResult = it } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) + obj.put("latitude", latestLatitude) + obj.put("longitude", latestLongitude) + obj.put("accuracy", latestAccuracy) + obj.put("isMocked", isMockedLocation) + + // GPS metadata + satelliteStatus?.let { + val gnss = JSObject() + gnss.put("satellitesInView", it.satellitesInView) + gnss.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gnss.put("constellationCounts", constellations) + obj.put("gnssData", gnss) + } + + // Wi-Fi metadata + wifiScanResult?.let { + val wifi = JSArray() + it.aps.forEach { ap -> + val a = JSObject() + a.put("ssid", ap.ssid) + a.put("bssid", ap.bssid) + a.put("rssi", ap.rssi) + ap.distance?.let { d -> a.put("distance", d) } + wifi.put(a) + } + obj.put("wifiData", wifi) + } + + // IMU metadata + latestImu?.let { + val imu = JSObject() + imu.put("accelX", it.accelX) + imu.put("accelY", it.accelY) + imu.put("accelZ", it.accelZ) + imu.put("gyroX", it.gyroX) + imu.put("gyroY", it.gyroY) + imu.put("gyroZ", it.gyroZ) + imu.put("speed", it.speed) + imu.put("acceleration", it.acceleration) + imu.put("directionRad", it.directionRad) + obj.put("imuData", imu) + } + + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614142835.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614142835.kt new file mode 100644 index 0000000..f397b15 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614142835.kt @@ -0,0 +1,166 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuData +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestImu: ImuData? = null + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + private var isMockedLocation = false + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { location, isMocked -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = if (isMocked) "MOCK" else "GNSS" + isMockedLocation = isMocked // sudah tersedia dari GpsStatusManager + latestTimestamp = location.time + fusionManager?.updateGpsPosition(latestLatitude, latestLongitude) + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onImuUpdate = { + latestImu = it + emitPositionUpdate() + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { + wifiScanResult = it + emitPositionUpdate() + } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) + obj.put("latitude", latestLatitude) + obj.put("longitude", latestLongitude) + obj.put("accuracy", latestAccuracy) + obj.put("isMocked", isMockedLocation) + + // GPS metadata + satelliteStatus?.let { + val gnss = JSObject() + gnss.put("satellitesInView", it.satellitesInView) + gnss.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gnss.put("constellationCounts", constellations) + obj.put("gnssData", gnss) + } + + // Wi-Fi metadata + wifiScanResult?.let { + val wifi = JSArray() + it.aps.forEach { ap -> + val a = JSObject() + a.put("ssid", ap.ssid) + a.put("bssid", ap.bssid) + a.put("rssi", ap.rssi) + ap.distance?.let { d -> a.put("distance", d) } + wifi.put(a) + } + obj.put("wifiData", wifi) + } + + // IMU metadata + latestImu?.let { + val imu = JSObject() + imu.put("accelX", it.accelX) + imu.put("accelY", it.accelY) + imu.put("accelZ", it.accelZ) + imu.put("gyroX", it.gyroX) + imu.put("gyroY", it.gyroY) + imu.put("gyroZ", it.gyroZ) + imu.put("speed", it.speed) + imu.put("acceleration", it.acceleration) + imu.put("directionRad", it.directionRad) + obj.put("imuData", imu) + } + + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614142847.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614142847.kt new file mode 100644 index 0000000..f397b15 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614142847.kt @@ -0,0 +1,166 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuData +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestImu: ImuData? = null + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + private var isMockedLocation = false + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { location, isMocked -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = if (isMocked) "MOCK" else "GNSS" + isMockedLocation = isMocked // sudah tersedia dari GpsStatusManager + latestTimestamp = location.time + fusionManager?.updateGpsPosition(latestLatitude, latestLongitude) + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onImuUpdate = { + latestImu = it + emitPositionUpdate() + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { + wifiScanResult = it + emitPositionUpdate() + } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) + obj.put("latitude", latestLatitude) + obj.put("longitude", latestLongitude) + obj.put("accuracy", latestAccuracy) + obj.put("isMocked", isMockedLocation) + + // GPS metadata + satelliteStatus?.let { + val gnss = JSObject() + gnss.put("satellitesInView", it.satellitesInView) + gnss.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gnss.put("constellationCounts", constellations) + obj.put("gnssData", gnss) + } + + // Wi-Fi metadata + wifiScanResult?.let { + val wifi = JSArray() + it.aps.forEach { ap -> + val a = JSObject() + a.put("ssid", ap.ssid) + a.put("bssid", ap.bssid) + a.put("rssi", ap.rssi) + ap.distance?.let { d -> a.put("distance", d) } + wifi.put(a) + } + obj.put("wifiData", wifi) + } + + // IMU metadata + latestImu?.let { + val imu = JSObject() + imu.put("accelX", it.accelX) + imu.put("accelY", it.accelY) + imu.put("accelZ", it.accelZ) + imu.put("gyroX", it.gyroX) + imu.put("gyroY", it.gyroY) + imu.put("gyroZ", it.gyroZ) + imu.put("speed", it.speed) + imu.put("acceleration", it.acceleration) + imu.put("directionRad", it.directionRad) + obj.put("imuData", imu) + } + + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614143835.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614143835.kt new file mode 100644 index 0000000..25d041f --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614143835.kt @@ -0,0 +1,168 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuData +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestImu: ImuData? = null + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + private var isMockedLocation = false + private var lastEmitTimestamp: Long = 0L + private val emitIntervalMs: Long = 500L + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { location, isMocked -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = if (isMocked) "MOCK" else "GNSS" + isMockedLocation = isMocked + latestTimestamp = location.time + fusionManager?.updateGpsPosition(latestLatitude, latestLongitude) + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onImuUpdate = { + latestImu = it + emitPositionUpdate() + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { + wifiScanResult = it + emitPositionUpdate() + } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + val now = System.currentTimeMillis() + if (now - lastEmitTimestamp < emitIntervalMs) return + lastEmitTimestamp = now + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) + obj.put("latitude", latestLatitude) + obj.put("longitude", latestLongitude) + obj.put("accuracy", latestAccuracy) + obj.put("isMocked", isMockedLocation) + + satelliteStatus?.let { + val gnss = JSObject() + gnss.put("satellitesInView", it.satellitesInView) + gnss.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gnss.put("constellationCounts", constellations) + obj.put("gnssData", gnss) + } + + wifiScanResult?.let { + val wifi = JSArray() + it.aps.forEach { ap -> + val a = JSObject() + a.put("ssid", ap.ssid) + a.put("bssid", ap.bssid) + a.put("rssi", ap.rssi) + ap.distance?.let { d -> a.put("distance", d) } + wifi.put(a) + } + obj.put("wifiData", wifi) + } + + latestImu?.let { + val imu = JSObject() + imu.put("accelX", it.accelX) + imu.put("accelY", it.accelY) + imu.put("accelZ", it.accelZ) + imu.put("gyroX", it.gyroX) + imu.put("gyroY", it.gyroY) + imu.put("gyroZ", it.gyroZ) + imu.put("speed", it.speed) + imu.put("acceleration", it.acceleration) + imu.put("directionRad", it.directionRad) + obj.put("imuData", imu) + } + + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614144416.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614144416.kt new file mode 100644 index 0000000..f557dec --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614144416.kt @@ -0,0 +1,168 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuData +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestImu: ImuData? = null + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + private var isMockedLocation = false + private var lastEmitTimestamp: Long = 0L + private val emitIntervalMs: Long = 500L + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { location, isMocked -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = if (isMocked) "MOCK" else "GNSS" + isMockedLocation = isMocked + latestTimestamp = location.time + fusionManager?.updateGpsPosition(latestLatitude, latestLongitude) + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onImuUpdate = { + latestImu = it + emitPositionUpdate() + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { + wifiScanResult = it + emitPositionUpdate() + } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + val now = System.currentTimeMillis() + if (now - lastEmitTimestamp < emitIntervalMs) return + lastEmitTimestamp = now + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) + obj.put("latitude", latestLatitude) + obj.put("longitude", latestLongitude) + obj.put("accuracy", latestAccuracy) + obj.put("isMocked", isMockedLocation) + + satelliteStatus?.let { + val gnss = JSObject() + gnss.put("satellitesInView", it.satellitesInView) + gnss.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gnss.put("constellationCounts", constellations) + obj.put("gnssData", gnss) + } + + latestImu?.let { + val imu = JSObject() + imu.put("accelX", it.accelX) + imu.put("accelY", it.accelY) + imu.put("accelZ", it.accelZ) + imu.put("gyroX", it.gyroX) + imu.put("gyroY", it.gyroY) + imu.put("gyroZ", it.gyroZ) + imu.put("speed", it.speed) + imu.put("acceleration", it.acceleration) + imu.put("directionRad", it.directionRad) + obj.put("imuData", imu) + } + + wifiScanResult?.let { + val wifi = JSArray() + it.aps.forEach { ap -> + val a = JSObject() + a.put("ssid", ap.ssid) + a.put("bssid", ap.bssid) + a.put("rssi", ap.rssi) + ap.distance?.let { d -> a.put("distance", d) } + wifi.put(a) + } + obj.put("wifiData", wifi) + } + + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614151111.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614151111.kt new file mode 100644 index 0000000..34d8dd2 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614151111.kt @@ -0,0 +1,168 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuData +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestImu: ImuData? = null + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + private var isMockedLocation = false + private var lastEmitTimestamp: Long = 0L + private val emitIntervalMs: Long = 500L + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { location, isMocked -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = if (isMocked) "MOCK" else "GNSS" + isMockedLocation = isMocked + latestTimestamp = location.time + fusionManager?.updateGpsPosition(latestLatitude, latestLongitude) + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onImuUpdate = { + latestImu = it + emitPositionUpdate() + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { + wifiScanResult = it + emitPositionUpdate() + } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + val now = System.currentTimeMillis() + if (now - lastEmitTimestamp < emitIntervalMs) return + lastEmitTimestamp = now + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) + obj.put("latitude", latestLatitude) + obj.put("longitude", latestLongitude) + obj.put("accuracy", latestAccuracy) + obj.put("isMocked", isMockedLocation) + + satelliteStatus?.let { + val gnss = JSObject() + gnss.put("satellitesInView", it.satellitesInView) + gnss.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gnss.put("constellationCounts", constellations) + obj.put("gnssData", gnss) + } + + latestImu?.let { + val imu = JSObject() + // imu.put("accelX", it.accelX) + // imu.put("accelY", it.accelY) + // imu.put("accelZ", it.accelZ) + // imu.put("gyroX", it.gyroX) + // imu.put("gyroY", it.gyroY) + // imu.put("gyroZ", it.gyroZ) + imu.put("speed", it.speed) + imu.put("acceleration", it.acceleration) + imu.put("directionRad", it.directionRad) + obj.put("imuData", imu) + } + + wifiScanResult?.let { + val wifi = JSArray() + it.aps.forEach { ap -> + val a = JSObject() + a.put("ssid", ap.ssid) + a.put("bssid", ap.bssid) + a.put("rssi", ap.rssi) + ap.distance?.let { d -> a.put("distance", d) } + wifi.put(a) + } + obj.put("wifiData", wifi) + } + + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614151149.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614151149.kt new file mode 100644 index 0000000..34d8dd2 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614151149.kt @@ -0,0 +1,168 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuData +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestImu: ImuData? = null + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + private var isMockedLocation = false + private var lastEmitTimestamp: Long = 0L + private val emitIntervalMs: Long = 500L + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { location, isMocked -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = if (isMocked) "MOCK" else "GNSS" + isMockedLocation = isMocked + latestTimestamp = location.time + fusionManager?.updateGpsPosition(latestLatitude, latestLongitude) + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onImuUpdate = { + latestImu = it + emitPositionUpdate() + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { + wifiScanResult = it + emitPositionUpdate() + } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + val now = System.currentTimeMillis() + if (now - lastEmitTimestamp < emitIntervalMs) return + lastEmitTimestamp = now + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) + obj.put("latitude", latestLatitude) + obj.put("longitude", latestLongitude) + obj.put("accuracy", latestAccuracy) + obj.put("isMocked", isMockedLocation) + + satelliteStatus?.let { + val gnss = JSObject() + gnss.put("satellitesInView", it.satellitesInView) + gnss.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gnss.put("constellationCounts", constellations) + obj.put("gnssData", gnss) + } + + latestImu?.let { + val imu = JSObject() + // imu.put("accelX", it.accelX) + // imu.put("accelY", it.accelY) + // imu.put("accelZ", it.accelZ) + // imu.put("gyroX", it.gyroX) + // imu.put("gyroY", it.gyroY) + // imu.put("gyroZ", it.gyroZ) + imu.put("speed", it.speed) + imu.put("acceleration", it.acceleration) + imu.put("directionRad", it.directionRad) + obj.put("imuData", imu) + } + + wifiScanResult?.let { + val wifi = JSArray() + it.aps.forEach { ap -> + val a = JSObject() + a.put("ssid", ap.ssid) + a.put("bssid", ap.bssid) + a.put("rssi", ap.rssi) + ap.distance?.let { d -> a.put("distance", d) } + wifi.put(a) + } + obj.put("wifiData", wifi) + } + + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614152152.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614152152.kt new file mode 100644 index 0000000..dc6d3ab --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation_20250614152152.kt @@ -0,0 +1,177 @@ +package com.dumon.plugin.geolocation + +import android.Manifest +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.dumon.plugin.geolocation.gps.GpsStatusManager +import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuData +import com.dumon.plugin.geolocation.imu.ImuSensorManager +import com.dumon.plugin.geolocation.wifi.WifiPositioningManager +import com.dumon.plugin.geolocation.wifi.WifiScanResult +import com.dumon.plugin.geolocation.fusion.SensorFusionManager +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "DumonGeolocation", + permissions = [ + Permission(strings = [ + Manifest.permission.ACCESS_FINE_LOCATION, + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_WIFI_STATE, + Manifest.permission.CHANGE_WIFI_STATE, + Manifest.permission.NEARBY_WIFI_DEVICES + ]) + ] +) +class DumonGeolocation : Plugin() { + + private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null + private var wifiManager: WifiPositioningManager? = null + private var fusionManager: SensorFusionManager? = null + + private var latestLatitude = 0.0 + private var latestLongitude = 0.0 + private var latestAccuracy = 999.0 + private var latestSource = "GNSS" + private var latestTimestamp: Long = 0L + + private var latestImu: ImuData? = null + private var satelliteStatus: SatelliteStatus? = null + private var wifiScanResult: WifiScanResult? = null + + private var isMockedLocation = false + private var lastEmitTimestamp: Long = 0L + private val emitIntervalMs: Long = 500L + + override fun load() { + gpsManager = GpsStatusManager( + context, + onSatelliteStatusUpdate = { satelliteStatus = it }, + onLocationUpdate = { location, isMocked -> + latestLatitude = location.latitude + latestLongitude = location.longitude + latestAccuracy = location.accuracy.toDouble() + latestSource = if (isMocked) "MOCK" else "GNSS" + isMockedLocation = isMocked + latestTimestamp = location.time + fusionManager?.updateGpsPosition(latestLatitude, latestLongitude) + emitPositionUpdate() + } + ) + + imuManager = ImuSensorManager( + context, + onImuUpdate = { + latestImu = it + emitPositionUpdate() + } + ) + + wifiManager = WifiPositioningManager( + context, + onWifiPositioningUpdate = { + wifiScanResult = it + emitPositionUpdate() + } + ) + + fusionManager = SensorFusionManager { lat, lon -> + latestLatitude = lat + latestLongitude = lon + latestAccuracy = 3.0 + latestSource = "FUSED" + latestTimestamp = System.currentTimeMillis() + emitPositionUpdate() + } + } + + @PluginMethod + fun startPositioning(call: PluginCall) { + gpsManager?.start() + imuManager?.start() + wifiManager?.startPeriodicScan(3000L) + call.resolve() + } + + @PluginMethod + fun stopPositioning(call: PluginCall) { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + call.resolve() + } + + @PluginMethod + fun getLatestPosition(call: PluginCall) { + call.resolve(buildPositionData()) + } + + private fun emitPositionUpdate() { + val now = System.currentTimeMillis() + if (now - lastEmitTimestamp < emitIntervalMs) return + lastEmitTimestamp = now + notifyListeners("onPositionUpdate", buildPositionData()) + } + + private fun buildPositionData(): JSObject { + val obj = JSObject() + obj.put("source", latestSource) + obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) + obj.put("latitude", latestLatitude) + obj.put("longitude", latestLongitude) + obj.put("accuracy", latestAccuracy) + obj.put("isMocked", isMockedLocation) + + latestImu?.let { + obj.put("speed", it.speed) + obj.put("acceleration", it.acceleration) + obj.put("directionRad", it.directionRad) + } + + // === Full Detail (commented out for future use) === + /* + satelliteStatus?.let { + val gnss = JSObject() + gnss.put("satellitesInView", it.satellitesInView) + gnss.put("usedInFix", it.usedInFix) + val constellations = JSObject() + it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) } + gnss.put("constellationCounts", constellations) + obj.put("gnssData", gnss) + } + + latestImu?.let { + val imu = JSObject() + imu.put("accelX", it.accelX) + imu.put("accelY", it.accelY) + imu.put("accelZ", it.accelZ) + imu.put("gyroX", it.gyroX) + imu.put("gyroY", it.gyroY) + imu.put("gyroZ", it.gyroZ) + imu.put("speed", it.speed) + imu.put("acceleration", it.acceleration) + imu.put("directionRad", it.directionRad) + obj.put("imuData", imu) + } + + wifiScanResult?.let { + val wifi = JSArray() + it.aps.forEach { ap -> + val a = JSObject() + a.put("ssid", ap.ssid) + a.put("bssid", ap.bssid) + a.put("rssi", ap.rssi) + ap.distance?.let { d -> a.put("distance", d) } + wifi.put(a) + } + obj.put("wifiData", wifi) + } + */ + + return obj + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/fusion/SensorFusionManager_20250614121146.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/fusion/SensorFusionManager_20250614121146.kt new file mode 100644 index 0000000..f8d494e --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/fusion/SensorFusionManager_20250614121146.kt @@ -0,0 +1,62 @@ +package com.dumon.plugin.geolocation.fusion + +import android.util.Log + +class SensorFusionManager( + private val onFusedPositionUpdate: (Double, Double) -> Unit = { _, _ -> } +) { + + // Kalman filter state + private var latEstimate = 0.0 + private var lonEstimate = 0.0 + private var latErrorEstimate = 1.0 + private var lonErrorEstimate = 1.0 + + private val processNoiseBase = 0.01 + private val measurementNoise = 3.0 // Estimasi error GPS biasa: ยฑ3m + + private var isFirstUpdate = true + private var lastUpdateTimestamp: Long = 0L + + fun updateGpsPosition(lat: Double, lon: Double) { + val currentTimestamp = System.currentTimeMillis() + val dtSeconds = if (lastUpdateTimestamp > 0) { + (currentTimestamp - lastUpdateTimestamp) / 1000.0 + } else { + 1.0 + } + lastUpdateTimestamp = currentTimestamp + + val processNoise = processNoiseBase * dtSeconds + + if (isFirstUpdate) { + latEstimate = lat + lonEstimate = lon + isFirstUpdate = false + } else { + // Kalman update untuk latitude + val latKalmanGain = latErrorEstimate / (latErrorEstimate + measurementNoise) + latEstimate += latKalmanGain * (lat - latEstimate) + latErrorEstimate = (1 - latKalmanGain) * latErrorEstimate + processNoise + + // Kalman update untuk longitude + val lonKalmanGain = lonErrorEstimate / (lonErrorEstimate + measurementNoise) + lonEstimate += lonKalmanGain * (lon - lonEstimate) + lonErrorEstimate = (1 - lonKalmanGain) * lonErrorEstimate + processNoise + } + + Log.d("SENSOR_FUSION", "Fused Lat: $latEstimate, Lon: $lonEstimate") + + // Emit hasil update + onFusedPositionUpdate(latEstimate, lonEstimate) + } + + fun reset() { + latEstimate = 0.0 + lonEstimate = 0.0 + latErrorEstimate = 1.0 + lonErrorEstimate = 1.0 + isFirstUpdate = true + lastUpdateTimestamp = 0L + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/fusion/SensorFusionManager_20250614135521.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/fusion/SensorFusionManager_20250614135521.kt new file mode 100644 index 0000000..32f4098 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/fusion/SensorFusionManager_20250614135521.kt @@ -0,0 +1,61 @@ +package com.dumon.plugin.geolocation.fusion + +import android.util.Log + +class SensorFusionManager( + private val onFusedPositionUpdate: (Double, Double) -> Unit = { _, _ -> } +) { + + // Kalman filter state + private var latEstimate = 0.0 + private var lonEstimate = 0.0 + private var latErrorEstimate = 1.0 + private var lonErrorEstimate = 1.0 + + private val processNoiseBase = 0.01 + private val measurementNoise = 3.0 // typical GPS error in meters + + private var isFirstUpdate = true + private var lastUpdateTimestamp: Long = 0L + + fun updateGpsPosition(lat: Double, lon: Double) { + val currentTimestamp = System.currentTimeMillis() + val dtSeconds = if (lastUpdateTimestamp > 0) { + (currentTimestamp - lastUpdateTimestamp) / 1000.0 + } else { + 1.0 + } + lastUpdateTimestamp = currentTimestamp + + val processNoise = processNoiseBase * dtSeconds + + if (isFirstUpdate) { + latEstimate = lat + lonEstimate = lon + isFirstUpdate = false + } else { + // Kalman update for latitude + val latKalmanGain = latErrorEstimate / (latErrorEstimate + measurementNoise) + latEstimate += latKalmanGain * (lat - latEstimate) + latErrorEstimate = (1 - latKalmanGain) * latErrorEstimate + processNoise + + // Kalman update for longitude + val lonKalmanGain = lonErrorEstimate / (lonErrorEstimate + measurementNoise) + lonEstimate += lonKalmanGain * (lon - lonEstimate) + lonErrorEstimate = (1 - lonKalmanGain) * lonErrorEstimate + processNoise + } + + Log.d("SENSOR_FUSION", "Fused Lat: $latEstimate, Lon: $lonEstimate") + + onFusedPositionUpdate(latEstimate, lonEstimate) + } + + fun reset() { + latEstimate = 0.0 + lonEstimate = 0.0 + latErrorEstimate = 1.0 + lonErrorEstimate = 1.0 + isFirstUpdate = true + lastUpdateTimestamp = 0L + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614101730.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614101730.kt new file mode 100644 index 0000000..5036b55 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614101730.kt @@ -0,0 +1,113 @@ +package com.dumon.plugin.geolocation.gps + +import android.annotation.SuppressLint +import android.content.Context +import android.location.GnssStatus +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.text.format.DateFormat +import android.util.Log + +class GpsStatusManager( + private val context: Context, + private val onSatelliteStatusUpdate: (String) -> Unit = {}, + private val onLocationUpdate: (String) -> Unit = {} +) { + + private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + private val gnssStatusCallback = object : GnssStatus.Callback() { + override fun onSatelliteStatusChanged(status: GnssStatus) { + val satelliteCount = status.satelliteCount + var usedInFixCount = 0 + val constellationCounts = mutableMapOf() + + for (i in 0 until satelliteCount) { + if (status.usedInFix(i)) usedInFixCount++ + val constellationType = status.getConstellationType(i) + constellationCounts[constellationType] = constellationCounts.getOrDefault(constellationType, 0) + 1 + } + + val info = buildString { + append("Satellites in view: $satelliteCount\n") + append("Used in fix: $usedInFixCount\n") + constellationCounts.forEach { (type, count) -> + append("${getConstellationName(type)} โ†’ $count\n") + } + } + + Log.d("GPS_STATUS", info) + onSatelliteStatusUpdate(info) + } + } + + private val locationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + val providerTag = when (location.provider) { + LocationManager.GPS_PROVIDER -> "[GPS]" + LocationManager.NETWORK_PROVIDER -> "[NET]" + else -> "[OTHER]" + } + + val timestamp = DateFormat.format("HH:mm:ss", System.currentTimeMillis()) + + val info = "$providerTag Lat: ${location.latitude}, Lon: ${location.longitude}, Acc: ${location.accuracy} m @ $timestamp" + Log.d("GPS_LOCATION", info) + onLocationUpdate(info) + } + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + } + + @SuppressLint("MissingPermission") + fun start() { + try { + locationManager.registerGnssStatusCallback(gnssStatusCallback, null) + + // Request location updates โ†’ GPS_PROVIDER + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 1000L, // minTime โ†’ 1 detik + 0f, // minDistance โ†’ 0 meter + locationListener + ) + + // Fallback โ†’ Network Provider + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + 3000L, // minTime โ†’ 3 detik + 10f, // minDistance โ†’ 10 meter + locationListener + ) + + Log.d("GPS_STATUS", "GPS + Network location tracking started") + } catch (e: SecurityException) { + Log.e("GPS_STATUS", "Missing permissions", e) + } + } + + fun stop() { + locationManager.unregisterGnssStatusCallback(gnssStatusCallback) + locationManager.removeUpdates(locationListener) + Log.d("GPS_STATUS", "GPS tracking stopped") + } + + private fun getConstellationName(type: Int): String { + return when (type) { + GnssStatus.CONSTELLATION_GPS -> "GPS" + GnssStatus.CONSTELLATION_SBAS -> "SBAS" + GnssStatus.CONSTELLATION_GLONASS -> "GLONASS" + GnssStatus.CONSTELLATION_QZSS -> "QZSS" + GnssStatus.CONSTELLATION_BEIDOU -> "BEIDOU" + GnssStatus.CONSTELLATION_GALILEO -> "GALILEO" + GnssStatus.CONSTELLATION_IRNSS -> "IRNSS" + else -> "Unknown ($type)" + } + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614111440.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614111440.kt new file mode 100644 index 0000000..65ea513 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614111440.kt @@ -0,0 +1,113 @@ +package com.dumon.plugin.geolocation.gps + +import android.annotation.SuppressLint +import android.content.Context +import android.location.GnssStatus +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.text.format.DateFormat +import android.util.Log + +class GpsStatusManager( + private val context: Context, + private val onSatelliteStatusUpdate: (String) -> Unit = {}, + private val onLocationUpdate: (Location) -> Unit = {} +) { + + private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + private val gnssStatusCallback = object : GnssStatus.Callback() { + override fun onSatelliteStatusChanged(status: GnssStatus) { + val satelliteCount = status.satelliteCount + var usedInFixCount = 0 + val constellationCounts = mutableMapOf() + + for (i in 0 until satelliteCount) { + if (status.usedInFix(i)) usedInFixCount++ + val constellationType = status.getConstellationType(i) + constellationCounts[constellationType] = constellationCounts.getOrDefault(constellationType, 0) + 1 + } + + val info = buildString { + append("Satellites in view: $satelliteCount\n") + append("Used in fix: $usedInFixCount\n") + constellationCounts.forEach { (type, count) -> + append("${getConstellationName(type)} โ†’ $count\n") + } + } + + Log.d("GPS_STATUS", info) + onSatelliteStatusUpdate(info) + } + } + + private val locationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + val providerTag = when (location.provider) { + LocationManager.GPS_PROVIDER -> "[GPS]" + LocationManager.NETWORK_PROVIDER -> "[NET]" + else -> "[OTHER]" + } + + val timestamp = DateFormat.format("HH:mm:ss", System.currentTimeMillis()) + + val info = "$providerTag Lat: ${location.latitude}, Lon: ${location.longitude}, Acc: ${location.accuracy} m @ $timestamp" + Log.d("GPS_LOCATION", info) + onLocationUpdate(info) + } + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + } + + @SuppressLint("MissingPermission") + fun start() { + try { + locationManager.registerGnssStatusCallback(gnssStatusCallback, null) + + // Request location updates โ†’ GPS_PROVIDER + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 1000L, // minTime โ†’ 1 detik + 0f, // minDistance โ†’ 0 meter + locationListener + ) + + // Fallback โ†’ Network Provider + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + 3000L, // minTime โ†’ 3 detik + 10f, // minDistance โ†’ 10 meter + locationListener + ) + + Log.d("GPS_STATUS", "GPS + Network location tracking started") + } catch (e: SecurityException) { + Log.e("GPS_STATUS", "Missing permissions", e) + } + } + + fun stop() { + locationManager.unregisterGnssStatusCallback(gnssStatusCallback) + locationManager.removeUpdates(locationListener) + Log.d("GPS_STATUS", "GPS tracking stopped") + } + + private fun getConstellationName(type: Int): String { + return when (type) { + GnssStatus.CONSTELLATION_GPS -> "GPS" + GnssStatus.CONSTELLATION_SBAS -> "SBAS" + GnssStatus.CONSTELLATION_GLONASS -> "GLONASS" + GnssStatus.CONSTELLATION_QZSS -> "QZSS" + GnssStatus.CONSTELLATION_BEIDOU -> "BEIDOU" + GnssStatus.CONSTELLATION_GALILEO -> "GALILEO" + GnssStatus.CONSTELLATION_IRNSS -> "IRNSS" + else -> "Unknown ($type)" + } + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614111540.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614111540.kt new file mode 100644 index 0000000..9643f12 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614111540.kt @@ -0,0 +1,113 @@ +package com.dumon.plugin.geolocation.gps + +import android.annotation.SuppressLint +import android.content.Context +import android.location.GnssStatus +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.text.format.DateFormat +import android.util.Log + +class GpsStatusManager( + private val context: Context, + private val onSatelliteStatusUpdate: (String) -> Unit = {}, + private val onLocationUpdate: (Location) -> Unit = {} +) { + + private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + private val gnssStatusCallback = object : GnssStatus.Callback() { + override fun onSatelliteStatusChanged(status: GnssStatus) { + val satelliteCount = status.satelliteCount + var usedInFixCount = 0 + val constellationCounts = mutableMapOf() + + for (i in 0 until satelliteCount) { + if (status.usedInFix(i)) usedInFixCount++ + val constellationType = status.getConstellationType(i) + constellationCounts[constellationType] = constellationCounts.getOrDefault(constellationType, 0) + 1 + } + + val info = buildString { + append("Satellites in view: $satelliteCount\n") + append("Used in fix: $usedInFixCount\n") + constellationCounts.forEach { (type, count) -> + append("${getConstellationName(type)} โ†’ $count\n") + } + } + + Log.d("GPS_STATUS", info) + onSatelliteStatusUpdate(info) + } + } + + private val locationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + val providerTag = when (location.provider) { + LocationManager.GPS_PROVIDER -> "[GPS]" + LocationManager.NETWORK_PROVIDER -> "[NET]" + else -> "[OTHER]" + } + + val timestamp = DateFormat.format("HH:mm:ss", System.currentTimeMillis()) + + val info = "$providerTag Lat: ${location.latitude}, Lon: ${location.longitude}, Acc: ${location.accuracy} m @ $timestamp" + Log.d("GPS_LOCATION", info) + onLocationUpdate(location) + } + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + } + + @SuppressLint("MissingPermission") + fun start() { + try { + locationManager.registerGnssStatusCallback(gnssStatusCallback, null) + + // Request location updates โ†’ GPS_PROVIDER + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 1000L, // minTime โ†’ 1 detik + 0f, // minDistance โ†’ 0 meter + locationListener + ) + + // Fallback โ†’ Network Provider + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + 3000L, // minTime โ†’ 3 detik + 10f, // minDistance โ†’ 10 meter + locationListener + ) + + Log.d("GPS_STATUS", "GPS + Network location tracking started") + } catch (e: SecurityException) { + Log.e("GPS_STATUS", "Missing permissions", e) + } + } + + fun stop() { + locationManager.unregisterGnssStatusCallback(gnssStatusCallback) + locationManager.removeUpdates(locationListener) + Log.d("GPS_STATUS", "GPS tracking stopped") + } + + private fun getConstellationName(type: Int): String { + return when (type) { + GnssStatus.CONSTELLATION_GPS -> "GPS" + GnssStatus.CONSTELLATION_SBAS -> "SBAS" + GnssStatus.CONSTELLATION_GLONASS -> "GLONASS" + GnssStatus.CONSTELLATION_QZSS -> "QZSS" + GnssStatus.CONSTELLATION_BEIDOU -> "BEIDOU" + GnssStatus.CONSTELLATION_GALILEO -> "GALILEO" + GnssStatus.CONSTELLATION_IRNSS -> "IRNSS" + else -> "Unknown ($type)" + } + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614112707.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614112707.kt new file mode 100644 index 0000000..601912d --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614112707.kt @@ -0,0 +1,117 @@ +package com.dumon.plugin.geolocation.gps + +import android.annotation.SuppressLint +import android.content.Context +import android.location.GnssStatus +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.text.format.DateFormat +import android.util.Log + +class GpsStatusManager( + private val context: Context, + // private val onSatelliteStatusUpdate: (String) -> Unit = {}, + private val onSatelliteStatusUpdate: (SatelliteStatus) -> Unit + private val onLocationUpdate: (Location) -> Unit = {} +) { + + private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + private val gnssStatusCallback = object : GnssStatus.Callback() { + override fun onSatelliteStatusChanged(status: GnssStatus) { + val satelliteCount = status.satelliteCount + var usedInFixCount = 0 + val constellationCounts = mutableMapOf() + + for (i in 0 until satelliteCount) { + if (status.usedInFix(i)) usedInFixCount++ + val constellationType = status.getConstellationType(i) + constellationCounts[constellationType] = constellationCounts.getOrDefault(constellationType, 0) + 1 + } + + val statusObj = SatelliteStatus( + satelliteCount, + usedInFixCount, + constellationCounts.mapKeys { getConstellationName(it.key) } + ) + + onSatelliteStatusUpdate(statusObj) + } + } + + private val locationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + val providerTag = when (location.provider) { + LocationManager.GPS_PROVIDER -> "[GPS]" + LocationManager.NETWORK_PROVIDER -> "[NET]" + else -> "[OTHER]" + } + + val timestamp = DateFormat.format("HH:mm:ss", System.currentTimeMillis()) + + val info = "$providerTag Lat: ${location.latitude}, Lon: ${location.longitude}, Acc: ${location.accuracy} m @ $timestamp" + Log.d("GPS_LOCATION", info) + onLocationUpdate(location) + } + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + } + + @SuppressLint("MissingPermission") + fun start() { + try { + locationManager.registerGnssStatusCallback(gnssStatusCallback, null) + + // Request location updates โ†’ GPS_PROVIDER + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 1000L, // minTime โ†’ 1 detik + 0f, // minDistance โ†’ 0 meter + locationListener + ) + + // Fallback โ†’ Network Provider + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + 3000L, // minTime โ†’ 3 detik + 10f, // minDistance โ†’ 10 meter + locationListener + ) + + Log.d("GPS_STATUS", "GPS + Network location tracking started") + } catch (e: SecurityException) { + Log.e("GPS_STATUS", "Missing permissions", e) + } + } + + fun stop() { + locationManager.unregisterGnssStatusCallback(gnssStatusCallback) + locationManager.removeUpdates(locationListener) + Log.d("GPS_STATUS", "GPS tracking stopped") + } + + private fun getConstellationName(type: Int): String { + return when (type) { + GnssStatus.CONSTELLATION_GPS -> "GPS" + GnssStatus.CONSTELLATION_SBAS -> "SBAS" + GnssStatus.CONSTELLATION_GLONASS -> "GLONASS" + GnssStatus.CONSTELLATION_QZSS -> "QZSS" + GnssStatus.CONSTELLATION_BEIDOU -> "BEIDOU" + GnssStatus.CONSTELLATION_GALILEO -> "GALILEO" + GnssStatus.CONSTELLATION_IRNSS -> "IRNSS" + else -> "Unknown ($type)" + } + } +} + +data class SatelliteStatus( + val satellitesInView: Int, + val usedInFix: Int, + val constellationCounts: Map +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614114439.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614114439.kt new file mode 100644 index 0000000..a6b724b --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614114439.kt @@ -0,0 +1,116 @@ +package com.dumon.plugin.geolocation.gps + +import android.annotation.SuppressLint +import android.content.Context +import android.location.GnssStatus +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.text.format.DateFormat +import android.util.Log + +class GpsStatusManager( + private val context: Context, + private val onSatelliteStatusUpdate: (SatelliteStatus) -> Unit = {}, + private val onLocationUpdate: (Location) -> Unit = {} +) { + + private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + private val gnssStatusCallback = object : GnssStatus.Callback() { + override fun onSatelliteStatusChanged(status: GnssStatus) { + val satelliteCount = status.satelliteCount + var usedInFixCount = 0 + val constellationCounts = mutableMapOf() + + for (i in 0 until satelliteCount) { + if (status.usedInFix(i)) usedInFixCount++ + val constellationType = status.getConstellationType(i) + constellationCounts[constellationType] = constellationCounts.getOrDefault(constellationType, 0) + 1 + } + + val statusObj = SatelliteStatus( + satelliteCount, + usedInFixCount, + constellationCounts.mapKeys { getConstellationName(it.key) } + ) + + onSatelliteStatusUpdate(statusObj) + } + } + + private val locationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + val providerTag = when (location.provider) { + LocationManager.GPS_PROVIDER -> "[GPS]" + LocationManager.NETWORK_PROVIDER -> "[NET]" + else -> "[OTHER]" + } + + val timestamp = DateFormat.format("HH:mm:ss", System.currentTimeMillis()) + + val info = "$providerTag Lat: ${location.latitude}, Lon: ${location.longitude}, Acc: ${location.accuracy} m @ $timestamp" + Log.d("GPS_LOCATION", info) + onLocationUpdate(location) + } + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + } + + @SuppressLint("MissingPermission") + fun start() { + try { + locationManager.registerGnssStatusCallback(gnssStatusCallback, null) + + // Request location updates โ†’ GPS_PROVIDER + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 1000L, // minTime โ†’ 1 detik + 0f, // minDistance โ†’ 0 meter + locationListener + ) + + // Fallback โ†’ Network Provider + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + 3000L, // minTime โ†’ 3 detik + 10f, // minDistance โ†’ 10 meter + locationListener + ) + + Log.d("GPS_STATUS", "GPS + Network location tracking started") + } catch (e: SecurityException) { + Log.e("GPS_STATUS", "Missing permissions", e) + } + } + + fun stop() { + locationManager.unregisterGnssStatusCallback(gnssStatusCallback) + locationManager.removeUpdates(locationListener) + Log.d("GPS_STATUS", "GPS tracking stopped") + } + + private fun getConstellationName(type: Int): String { + return when (type) { + GnssStatus.CONSTELLATION_GPS -> "GPS" + GnssStatus.CONSTELLATION_SBAS -> "SBAS" + GnssStatus.CONSTELLATION_GLONASS -> "GLONASS" + GnssStatus.CONSTELLATION_QZSS -> "QZSS" + GnssStatus.CONSTELLATION_BEIDOU -> "BEIDOU" + GnssStatus.CONSTELLATION_GALILEO -> "GALILEO" + GnssStatus.CONSTELLATION_IRNSS -> "IRNSS" + else -> "Unknown ($type)" + } + } +} + +data class SatelliteStatus( + val satellitesInView: Int, + val usedInFix: Int, + val constellationCounts: Map +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614115742.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614115742.kt new file mode 100644 index 0000000..4a1df0f --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614115742.kt @@ -0,0 +1,116 @@ +package com.dumon.plugin.geolocation.gps + +import android.annotation.SuppressLint +import android.content.Context +import android.location.GnssStatus +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.util.Log + +class GpsStatusManager( + private val context: Context, + private val onSatelliteStatusUpdate: (SatelliteStatus) -> Unit = {}, + private val onLocationUpdate: (Location) -> Unit = {} +) { + + private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + private val gnssStatusCallback = object : GnssStatus.Callback() { + override fun onSatelliteStatusChanged(status: GnssStatus) { + val satelliteCount = status.satelliteCount + var usedInFix = 0 + val constellationCounts = mutableMapOf() + + for (i in 0 until satelliteCount) { + if (status.usedInFix(i)) usedInFix++ + val constellation = getConstellationName(status.getConstellationType(i)) + constellationCounts[constellation] = constellationCounts.getOrDefault(constellation, 0) + 1 + } + + Log.d("GPS_STATUS", "Satellites in View: $satelliteCount | Used in Fix: $usedInFix") + Log.d("GPS_STATUS", "Constellation Breakdown: $constellationCounts") + + val statusObj = SatelliteStatus( + satellitesInView = satelliteCount, + usedInFix = usedInFix, + constellationCounts = constellationCounts + ) + + onSatelliteStatusUpdate(statusObj) + } + } + + private val locationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + val tag = when (location.provider) { + LocationManager.GPS_PROVIDER -> "[GPS]" + LocationManager.NETWORK_PROVIDER -> "[NET]" + else -> "[OTHER]" + } + + val info = "$tag Lat=${location.latitude}, Lon=${location.longitude}, Acc=${location.accuracy}m" + Log.d("GPS_LOCATION", info) + + onLocationUpdate(location) + } + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + } + + @SuppressLint("MissingPermission") + fun start() { + try { + locationManager.registerGnssStatusCallback(gnssStatusCallback, null) + + // GPS_PROVIDER updates + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 1000L, // 1 detik + 0f, + locationListener + ) + + // NETWORK_PROVIDER fallback + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + 3000L, + 10f, + locationListener + ) + + Log.d("GPS_STATUS", "GPS and Network location tracking started") + } catch (e: SecurityException) { + Log.e("GPS_STATUS", "Missing location permissions", e) + } + } + + fun stop() { + locationManager.unregisterGnssStatusCallback(gnssStatusCallback) + locationManager.removeUpdates(locationListener) + Log.d("GPS_STATUS", "Location tracking stopped") + } + + private fun getConstellationName(type: Int): String { + return when (type) { + GnssStatus.CONSTELLATION_GPS -> "GPS" + GnssStatus.CONSTELLATION_SBAS -> "SBAS" + GnssStatus.CONSTELLATION_GLONASS -> "GLONASS" + GnssStatus.CONSTELLATION_QZSS -> "QZSS" + GnssStatus.CONSTELLATION_BEIDOU -> "BEIDOU" + GnssStatus.CONSTELLATION_GALILEO -> "GALILEO" + GnssStatus.CONSTELLATION_IRNSS -> "IRNSS" + else -> "Unknown($type)" + } + } +} + +data class SatelliteStatus( + val satellitesInView: Int, + val usedInFix: Int, + val constellationCounts: Map +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614122840.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614122840.kt new file mode 100644 index 0000000..e7fcef9 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614122840.kt @@ -0,0 +1,129 @@ +package com.dumon.plugin.geolocation.gps + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.location.GnssStatus +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import android.os.Build +import android.text.format.DateFormat +import android.util.Log +import androidx.core.app.ActivityCompat + +class GpsStatusManager( + private val context: Context, + private val onSatelliteStatusUpdate: (SatelliteStatus) -> Unit = {}, + private val onLocationUpdate: (Location, Boolean) -> Unit = { _, _ -> } +) { + + private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + private val gnssStatusCallback = object : GnssStatus.Callback() { + override fun onSatelliteStatusChanged(status: GnssStatus) { + val satelliteCount = status.satelliteCount + var usedInFixCount = 0 + val constellationCounts = mutableMapOf() + + for (i in 0 until satelliteCount) { + if (status.usedInFix(i)) usedInFixCount++ + val constellationType = status.getConstellationType(i) + constellationCounts[constellationType] = constellationCounts.getOrDefault(constellationType, 0) + 1 + } + + val statusObj = SatelliteStatus( + satelliteCount, + usedInFixCount, + constellationCounts.mapKeys { getConstellationName(it.key) } + ) + + onSatelliteStatusUpdate(statusObj) + } + } + + private val locationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + val providerTag = when (location.provider) { + LocationManager.GPS_PROVIDER -> "[GPS]" + LocationManager.NETWORK_PROVIDER -> "[NET]" + else -> "[OTHER]" + } + + val timestamp = DateFormat.format("HH:mm:ss", System.currentTimeMillis()) + val isMocked = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + location.isMock // API 31+ + } else { + location.isFromMockProvider // Deprecated, tapi fallback + } + + val info = "$providerTag Lat: ${location.latitude}, Lon: ${location.longitude}, Acc: ${location.accuracy} m @ $timestamp | Mock=$isMocked" + Log.d("GPS_LOCATION", info) + + onLocationUpdate(location, isMocked) + } + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + } + + @SuppressLint("MissingPermission") + fun start() { + try { + if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + Log.e("GPS_STATUS", "Missing location permissions") + return + } + + locationManager.registerGnssStatusCallback(gnssStatusCallback, null) + + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 1000L, + 0f, + locationListener + ) + + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + 3000L, + 10f, + locationListener + ) + + Log.d("GPS_STATUS", "GPS + Network location tracking started") + } catch (e: SecurityException) { + Log.e("GPS_STATUS", "SecurityException", e) + } + } + + fun stop() { + locationManager.unregisterGnssStatusCallback(gnssStatusCallback) + locationManager.removeUpdates(locationListener) + Log.d("GPS_STATUS", "GPS tracking stopped") + } + + private fun getConstellationName(type: Int): String { + return when (type) { + GnssStatus.CONSTELLATION_GPS -> "GPS" + GnssStatus.CONSTELLATION_SBAS -> "SBAS" + GnssStatus.CONSTELLATION_GLONASS -> "GLONASS" + GnssStatus.CONSTELLATION_QZSS -> "QZSS" + GnssStatus.CONSTELLATION_BEIDOU -> "BEIDOU" + GnssStatus.CONSTELLATION_GALILEO -> "GALILEO" + GnssStatus.CONSTELLATION_IRNSS -> "IRNSS" + else -> "Unknown ($type)" + } + } +} + +data class SatelliteStatus( + val satellitesInView: Int, + val usedInFix: Int, + val constellationCounts: Map +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614135453.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614135453.kt new file mode 100644 index 0000000..4240646 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager_20250614135453.kt @@ -0,0 +1,129 @@ +package com.dumon.plugin.geolocation.gps + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.location.GnssStatus +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Build +import android.os.Bundle +import android.text.format.DateFormat +import android.util.Log +import androidx.core.app.ActivityCompat + +class GpsStatusManager( + private val context: Context, + private val onSatelliteStatusUpdate: (SatelliteStatus) -> Unit = {}, + private val onLocationUpdate: (Location, Boolean) -> Unit = { _, _ -> } +) { + + private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + + private val gnssStatusCallback = object : GnssStatus.Callback() { + override fun onSatelliteStatusChanged(status: GnssStatus) { + val satelliteCount = status.satelliteCount + var usedInFixCount = 0 + val constellationCounts = mutableMapOf() + + for (i in 0 until satelliteCount) { + if (status.usedInFix(i)) usedInFixCount++ + val constellationType = status.getConstellationType(i) + constellationCounts[constellationType] = constellationCounts.getOrDefault(constellationType, 0) + 1 + } + + val statusObj = SatelliteStatus( + satellitesInView = satelliteCount, + usedInFix = usedInFixCount, + constellationCounts = constellationCounts.mapKeys { getConstellationName(it.key) } + ) + + onSatelliteStatusUpdate(statusObj) + } + } + + private val locationListener = object : LocationListener { + override fun onLocationChanged(location: Location) { + val providerTag = when (location.provider) { + LocationManager.GPS_PROVIDER -> "[GPS]" + LocationManager.NETWORK_PROVIDER -> "[NET]" + else -> "[OTHER]" + } + + val timestamp = DateFormat.format("HH:mm:ss", System.currentTimeMillis()) + val isMocked = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + location.isMock // API 31+ + } else { + location.isFromMockProvider // Fallback + } + + val info = "$providerTag Lat: ${location.latitude}, Lon: ${location.longitude}, Acc: ${location.accuracy} m @ $timestamp | Mock=$isMocked" + Log.d("GPS_LOCATION", info) + + onLocationUpdate(location, isMocked) + } + + @Deprecated("Deprecated in Java") + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {} + + override fun onProviderEnabled(provider: String) {} + override fun onProviderDisabled(provider: String) {} + } + + @SuppressLint("MissingPermission") + fun start() { + try { + if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + Log.e("GPS_STATUS", "Missing location permissions") + return + } + + locationManager.registerGnssStatusCallback(gnssStatusCallback, null) + + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 1000L, + 0f, + locationListener + ) + + locationManager.requestLocationUpdates( + LocationManager.NETWORK_PROVIDER, + 3000L, + 10f, + locationListener + ) + + Log.d("GPS_STATUS", "GPS + Network location tracking started") + } catch (e: SecurityException) { + Log.e("GPS_STATUS", "SecurityException", e) + } + } + + fun stop() { + locationManager.unregisterGnssStatusCallback(gnssStatusCallback) + locationManager.removeUpdates(locationListener) + Log.d("GPS_STATUS", "GPS tracking stopped") + } + + private fun getConstellationName(type: Int): String { + return when (type) { + GnssStatus.CONSTELLATION_GPS -> "GPS" + GnssStatus.CONSTELLATION_SBAS -> "SBAS" + GnssStatus.CONSTELLATION_GLONASS -> "GLONASS" + GnssStatus.CONSTELLATION_QZSS -> "QZSS" + GnssStatus.CONSTELLATION_BEIDOU -> "BEIDOU" + GnssStatus.CONSTELLATION_GALILEO -> "GALILEO" + GnssStatus.CONSTELLATION_IRNSS -> "IRNSS" + else -> "Unknown ($type)" + } + } +} + +data class SatelliteStatus( + val satellitesInView: Int, + val usedInFix: Int, + val constellationCounts: Map +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614101730.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614101730.kt new file mode 100644 index 0000000..b400e74 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614101730.kt @@ -0,0 +1,56 @@ +package com.dumon.plugin.geolocation.imu + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log + +class ImuSensorManager( + private val context: Context, + private val onAccelerometerUpdate: (String) -> Unit = {}, + private val onGyroscopeUpdate: (String) -> Unit = {} +) : SensorEventListener { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION) + private val gyroscope: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + + fun start() { + accelerometer?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI) + } + gyroscope?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI) + } + Log.d("IMU_SENSOR", "IMU sensor tracking started") + } + + fun stop() { + sensorManager.unregisterListener(this) + Log.d("IMU_SENSOR", "IMU sensor tracking stopped") + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + when (it.sensor.type) { + Sensor.TYPE_LINEAR_ACCELERATION -> { + val info = "Accel x: %.2f, y: %.2f, z: %.2f m/sยฒ".format(it.values[0], it.values[1], it.values[2]) + Log.d("IMU_SENSOR", info) + onAccelerometerUpdate(info) + } + + Sensor.TYPE_GYROSCOPE -> { + val info = "Gyro x: %.2f, y: %.2f, z: %.2f rad/s".format(it.values[0], it.values[1], it.values[2]) + Log.d("IMU_SENSOR", info) + onGyroscopeUpdate(info) + } + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not used for now + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614115457.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614115457.kt new file mode 100644 index 0000000..f858366 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614115457.kt @@ -0,0 +1,54 @@ +package com.dumon.plugin.geolocation.imu + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log + +class ImuSensorManager( + private val context: Context, + private val onAccelerometerUpdate: (FloatArray) -> Unit, + private val onGyroscopeUpdate: (FloatArray) -> Unit +) : SensorEventListener { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION) + private val gyroscope: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + + fun start() { + accelerometer?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI) + } + gyroscope?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI) + } + Log.d("IMU_SENSOR", "IMU sensor tracking started") + } + + fun stop() { + sensorManager.unregisterListener(this) + Log.d("IMU_SENSOR", "IMU sensor tracking stopped") + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + when (it.sensor.type) { + Sensor.TYPE_LINEAR_ACCELERATION -> { + onAccelerometerUpdate(it.values.copyOf()) + Log.d("IMU_SENSOR", "Accel x: %.2f y: %.2f z: %.2f m/sยฒ".format(it.values[0], it.values[1], it.values[2])) + } + + Sensor.TYPE_GYROSCOPE -> { + onGyroscopeUpdate(it.values.copyOf()) + Log.d("IMU_SENSOR", "Gyro x: %.2f y: %.2f z: %.2f rad/s".format(it.values[0], it.values[1], it.values[2])) + } + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not used for now + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614135424.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614135424.kt new file mode 100644 index 0000000..daa6b6d --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614135424.kt @@ -0,0 +1,114 @@ +package com.dumon.plugin.geolocation.imu + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log +import kotlin.math.* + +class ImuSensorManager( + private val context: Context, + private val onImuUpdate: (ImuData) -> Unit +) : SensorEventListener { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION) + private val gyroscope: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + + private var lastAccel = FloatArray(3) { 0f } + private var lastGyro = FloatArray(3) { 0f } + + private var lastAccelTimestamp: Long = 0L + private var velocity = FloatArray(3) { 0f } + + fun start() { + accelerometer?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI) + } + gyroscope?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_UI) + } + Log.d("IMU_SENSOR", "IMU sensor tracking started") + } + + fun stop() { + sensorManager.unregisterListener(this) + Log.d("IMU_SENSOR", "IMU sensor tracking stopped") + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + when (it.sensor.type) { + Sensor.TYPE_LINEAR_ACCELERATION -> handleAccelerometer(it) + Sensor.TYPE_GYROSCOPE -> handleGyroscope(it) + } + } + } + + private fun handleAccelerometer(event: SensorEvent) { + val currentTime = event.timestamp + + if (lastAccelTimestamp != 0L) { + val dt = (currentTime - lastAccelTimestamp) / 1_000_000_000f // convert ns to seconds + for (i in 0..2) { + velocity[i] += event.values[i] * dt + } + } + + lastAccelTimestamp = currentTime + lastAccel = event.values.copyOf() + + val speed = sqrt(velocity[0].pow(2) + velocity[1].pow(2) + velocity[2].pow(2)) + val acceleration = sqrt( + lastAccel[0].pow(2) + lastAccel[1].pow(2) + lastAccel[2].pow(2) + ) + + emitCombinedImuData(speed, acceleration) + + Log.d("IMU_SENSOR", "Accel x: %.2f y: %.2f z: %.2f m/sยฒ | Speed: %.2f m/s | AccelMag: %.2f m/sยฒ".format( + lastAccel[0], lastAccel[1], lastAccel[2], speed, acceleration + )) + } + + private fun handleGyroscope(event: SensorEvent) { + lastGyro = event.values.copyOf() + Log.d("IMU_SENSOR", "Gyro x: %.2f y: %.2f z: %.2f rad/s".format( + lastGyro[0], lastGyro[1], lastGyro[2] + )) + } + + private fun emitCombinedImuData(speed: Float, acceleration: Float) { + val directionRad = atan2(lastGyro[1], lastGyro[0]) + onImuUpdate( + ImuData( + accelX = lastAccel[0], + accelY = lastAccel[1], + accelZ = lastAccel[2], + gyroX = lastGyro[0], + gyroY = lastGyro[1], + gyroZ = lastGyro[2], + speed = speed, + acceleration = acceleration, + directionRad = directionRad + ) + ) + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not used + } +} + +data class ImuData( + val accelX: Float, + val accelY: Float, + val accelZ: Float, + val gyroX: Float, + val gyroY: Float, + val gyroZ: Float, + val speed: Float, + val acceleration: Float, + val directionRad: Float +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614141651.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614141651.kt new file mode 100644 index 0000000..6513520 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614141651.kt @@ -0,0 +1,114 @@ +package com.dumon.plugin.geolocation.imu + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log +import kotlin.math.* + +class ImuSensorManager( + private val context: Context, + private val onImuUpdate: (ImuData) -> Unit +) : SensorEventListener { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION) + private val gyroscope: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + + private var lastAccel = FloatArray(3) { 0f } + private var lastGyro = FloatArray(3) { 0f } + + private var lastAccelTimestamp: Long = 0L + private var velocity = FloatArray(3) { 0f } + + fun start() { + accelerometer?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) + } + gyroscope?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) + } + Log.d("IMU_SENSOR", "IMU sensor tracking started") + } + + fun stop() { + sensorManager.unregisterListener(this) + Log.d("IMU_SENSOR", "IMU sensor tracking stopped") + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + when (it.sensor.type) { + Sensor.TYPE_LINEAR_ACCELERATION -> handleAccelerometer(it) + Sensor.TYPE_GYROSCOPE -> handleGyroscope(it) + } + } + } + + private fun handleAccelerometer(event: SensorEvent) { + val currentTime = event.timestamp + + if (lastAccelTimestamp != 0L) { + val dt = (currentTime - lastAccelTimestamp) / 1_000_000_000f // convert ns to seconds + for (i in 0..2) { + velocity[i] += event.values[i] * dt + } + } + + lastAccelTimestamp = currentTime + lastAccel = event.values.copyOf() + + val speed = sqrt(velocity[0].pow(2) + velocity[1].pow(2) + velocity[2].pow(2)) + val acceleration = sqrt( + lastAccel[0].pow(2) + lastAccel[1].pow(2) + lastAccel[2].pow(2) + ) + + emitCombinedImuData(speed, acceleration) + + Log.d("IMU_SENSOR", "Accel x: %.3f y: %.3f z: %.3f m/sยฒ | Speed: %.3f m/s | AccelMag: %.3f m/sยฒ".format( + lastAccel[0], lastAccel[1], lastAccel[2], speed, acceleration + )) + } + + private fun handleGyroscope(event: SensorEvent) { + lastGyro = event.values.copyOf() + Log.d("IMU_SENSOR", "Gyro x: %.3f y: %.3f z: %.3f rad/s".format( + lastGyro[0], lastGyro[1], lastGyro[2] + )) + } + + private fun emitCombinedImuData(speed: Float, acceleration: Float) { + val directionRad = atan2(lastGyro[1], lastGyro[0]) + onImuUpdate( + ImuData( + accelX = lastAccel[0], + accelY = lastAccel[1], + accelZ = lastAccel[2], + gyroX = lastGyro[0], + gyroY = lastGyro[1], + gyroZ = lastGyro[2], + speed = speed, + acceleration = acceleration, + directionRad = directionRad + ) + ) + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not used + } +} + +data class ImuData( + val accelX: Float, + val accelY: Float, + val accelZ: Float, + val gyroX: Float, + val gyroY: Float, + val gyroZ: Float, + val speed: Float, + val acceleration: Float, + val directionRad: Float +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614143756.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614143756.kt new file mode 100644 index 0000000..124dc2c --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614143756.kt @@ -0,0 +1,119 @@ +package com.dumon.plugin.geolocation.imu + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log +import kotlin.math.* + +class ImuSensorManager( + private val context: Context, + private val onImuUpdate: (ImuData) -> Unit +) : SensorEventListener { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val accelerometer: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION) + private val gyroscope: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + + private var lastAccel = FloatArray(3) { 0f } + private var lastGyro = FloatArray(3) { 0f } + + private var lastAccelTimestamp: Long = 0L + private var velocity = FloatArray(3) { 0f } + + private val accelLowPass = FloatArray(3) { 0f } + private val alpha = 0.8f // Low-pass filter constant + + fun start() { + accelerometer?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) + } + gyroscope?.let { + sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) + } + Log.d("IMU_SENSOR", "IMU sensor tracking started") + } + + fun stop() { + sensorManager.unregisterListener(this) + Log.d("IMU_SENSOR", "IMU sensor tracking stopped") + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + when (it.sensor.type) { + Sensor.TYPE_LINEAR_ACCELERATION -> handleAccelerometer(it) + Sensor.TYPE_GYROSCOPE -> handleGyroscope(it) + } + } + } + + private fun handleAccelerometer(event: SensorEvent) { + val currentTime = event.timestamp + + for (i in 0..2) { + accelLowPass[i] = alpha * accelLowPass[i] + (1 - alpha) * event.values[i] + } + + if (lastAccelTimestamp != 0L) { + val dt = (currentTime - lastAccelTimestamp) / 1_000_000_000f // convert ns to seconds + for (i in 0..2) { + velocity[i] += accelLowPass[i] * dt + } + } + + lastAccelTimestamp = currentTime + lastAccel = accelLowPass.copyOf() + + val speed = velocity.map { it.pow(2) }.sum().pow(0.5f).coerceIn(0f, 50f) + val acceleration = lastAccel.map { it.pow(2) }.sum().pow(0.5f).coerceIn(0f, 20f) + + emitCombinedImuData(speed, acceleration) + + Log.d("IMU_SENSOR", "Accel x: %.3f y: %.3f z: %.3f m/sยฒ | Speed: %.3f m/s | AccelMag: %.3f m/sยฒ".format( + lastAccel[0], lastAccel[1], lastAccel[2], speed, acceleration + )) + } + + private fun handleGyroscope(event: SensorEvent) { + lastGyro = event.values.copyOf() + Log.d("IMU_SENSOR", "Gyro x: %.3f y: %.3f z: %.3f rad/s".format( + lastGyro[0], lastGyro[1], lastGyro[2] + )) + } + + private fun emitCombinedImuData(speed: Float, acceleration: Float) { + val directionRad = if (speed > 0.5f) atan2(lastGyro[1], lastGyro[0]) else null + onImuUpdate( + ImuData( + accelX = lastAccel[0], + accelY = lastAccel[1], + accelZ = lastAccel[2], + gyroX = lastGyro[0], + gyroY = lastGyro[1], + gyroZ = lastGyro[2], + speed = speed, + acceleration = acceleration, + directionRad = directionRad ?: 0f + ) + ) + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not used + } +} + +data class ImuData( + val accelX: Float, + val accelY: Float, + val accelZ: Float, + val gyroX: Float, + val gyroY: Float, + val gyroZ: Float, + val speed: Float, + val acceleration: Float, + val directionRad: Float +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614144348.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614144348.kt new file mode 100644 index 0000000..6155f5e --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614144348.kt @@ -0,0 +1,126 @@ +package com.dumon.plugin.geolocation.imu + +import android.content.Context +import android.hardware.* +import android.util.Log +import kotlin.math.* + +class ImuSensorManager( + private val context: Context, + private val onImuUpdate: (ImuData) -> Unit +) : SensorEventListener { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION) + private val gyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + private val rotationVector = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + + private var lastAccel = FloatArray(3) { 0f } + private var lastGyro = FloatArray(3) { 0f } + private var lastAccelTimestamp: Long = 0L + private var velocity = FloatArray(3) { 0f } + private val accelLowPass = FloatArray(3) { 0f } + private val alpha = 0.8f + + private var latestDirectionRad = 0f + + fun start() { + accelerometer?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } + gyroscope?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } + rotationVector?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } + Log.d("IMU_SENSOR", "IMU sensor tracking started") + } + + fun stop() { + sensorManager.unregisterListener(this) + Log.d("IMU_SENSOR", "IMU sensor tracking stopped") + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + when (it.sensor.type) { + Sensor.TYPE_LINEAR_ACCELERATION -> handleAccelerometer(it) + Sensor.TYPE_GYROSCOPE -> handleGyroscope(it) + Sensor.TYPE_ROTATION_VECTOR -> handleRotationVector(it) + } + } + } + + private fun handleAccelerometer(event: SensorEvent) { + val currentTime = event.timestamp + val threshold = 0.1f + + for (i in 0..2) { + accelLowPass[i] = alpha * accelLowPass[i] + (1 - alpha) * event.values[i] + } + + if (lastAccelTimestamp != 0L) { + val dt = (currentTime - lastAccelTimestamp) / 1_000_000_000f + for (i in 0..2) { + val acc = accelLowPass[i] + if (abs(acc) > threshold) { + velocity[i] += acc * dt + } + } + } + + lastAccelTimestamp = currentTime + lastAccel = accelLowPass.copyOf() + + val speed = velocity.map { it * it }.sum().pow(0.5f).coerceIn(0f, 50f) + val acceleration = lastAccel.map { it * it }.sum().pow(0.5f).coerceIn(0f, 20f) + + val directionRad = if (speed > 0.3f) atan2(velocity[1], velocity[0]) else latestDirectionRad + latestDirectionRad = directionRad + + emitCombinedImuData(speed, acceleration, directionRad) + + Log.d("IMU_SENSOR", "Accel x: %.3f y: %.3f z: %.3f m/sยฒ | Speed: %.3f m/s | AccelMag: %.3f m/sยฒ | Dir: %.2f rad".format( + lastAccel[0], lastAccel[1], lastAccel[2], speed, acceleration, directionRad + )) + } + + private fun handleGyroscope(event: SensorEvent) { + lastGyro = event.values.copyOf() + } + + private fun handleRotationVector(event: SensorEvent) { + val rotationMatrix = FloatArray(9) + SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values) + val orientation = FloatArray(3) + SensorManager.getOrientation(rotationMatrix, orientation) + latestDirectionRad = orientation[0] // azimuth + } + + private fun emitCombinedImuData(speed: Float, acceleration: Float, directionRad: Float) { + onImuUpdate( + ImuData( + accelX = lastAccel[0], + accelY = lastAccel[1], + accelZ = lastAccel[2], + gyroX = lastGyro[0], + gyroY = lastGyro[1], + gyroZ = lastGyro[2], + speed = speed, + acceleration = acceleration, + directionRad = directionRad + ) + ) + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not used + } +} + +data class ImuData( + val accelX: Float, + val accelY: Float, + val accelZ: Float, + val gyroX: Float, + val gyroY: Float, + val gyroZ: Float, + val speed: Float, + val acceleration: Float, + val directionRad: Float +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614145144.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614145144.kt new file mode 100644 index 0000000..26e48ae --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614145144.kt @@ -0,0 +1,141 @@ +package com.dumon.plugin.geolocation.imu + +import android.content.Context +import android.hardware.* +import android.util.Log +import kotlin.math.* + +class ImuSensorManager( + private val context: Context, + private val onImuUpdate: (ImuData) -> Unit +) : SensorEventListener { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION) + private val gyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + private val rotationVector = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + + private var lastAccel = FloatArray(3) { 0f } + private var lastGyro = FloatArray(3) { 0f } + private var velocity = FloatArray(3) { 0f } + private var lastAccelTimestamp: Long = 0L + + private val accelLowPass = FloatArray(3) { 0f } + private val alpha = 0.85f + private val accelerationThreshold = 0.12f + private val idleTimeoutNs = 2_000_000_000L // 2 seconds + private var lastActiveTimestamp: Long = 0L + + private var latestDirectionRad = 0f + private var latestSpeed = 0f + + fun start() { + accelerometer?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } + gyroscope?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } + rotationVector?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } + Log.d("IMU_SENSOR", "IMU sensor tracking started") + } + + fun stop() { + sensorManager.unregisterListener(this) + Log.d("IMU_SENSOR", "IMU sensor tracking stopped") + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + when (it.sensor.type) { + Sensor.TYPE_LINEAR_ACCELERATION -> handleAccelerometer(it) + Sensor.TYPE_GYROSCOPE -> handleGyroscope(it) + Sensor.TYPE_ROTATION_VECTOR -> handleRotationVector(it) + } + } + } + + private fun handleAccelerometer(event: SensorEvent) { + val currentTime = event.timestamp + + for (i in 0..2) { + accelLowPass[i] = alpha * accelLowPass[i] + (1 - alpha) * event.values[i] + } + + val accMag = sqrt(accelLowPass.map { it * it }.sum()) + + if (accMag > accelerationThreshold) { + if (lastAccelTimestamp != 0L) { + val dt = (currentTime - lastAccelTimestamp) / 1_000_000_000f + for (i in 0..2) { + velocity[i] += accelLowPass[i] * dt + } + } + lastActiveTimestamp = currentTime + } else { + // If idle too long, decay velocity to zero + if (currentTime - lastActiveTimestamp > idleTimeoutNs) { + for (i in 0..2) velocity[i] = 0f + } else { + // Apply damping to reduce drift + for (i in 0..2) velocity[i] *= 0.96f + } + } + + lastAccelTimestamp = currentTime + lastAccel = accelLowPass.copyOf() + + val speed = velocity.map { it * it }.sum().pow(0.5f).coerceIn(0f, 30f) + val acceleration = accMag.coerceIn(0f, 20f) + + val directionRad = if (speed > 0.3f) atan2(velocity[1], velocity[0]) else latestDirectionRad + latestDirectionRad = directionRad + latestSpeed = speed + + emitCombinedImuData(speed, acceleration, directionRad) + + Log.d("IMU_SENSOR", "Accel x: %.3f y: %.3f z: %.3f | Speed: %.3f | AccelMag: %.3f | Dir: %.2f rad".format( + lastAccel[0], lastAccel[1], lastAccel[2], speed, acceleration, directionRad + )) + } + + private fun handleGyroscope(event: SensorEvent) { + lastGyro = event.values.copyOf() + } + + private fun handleRotationVector(event: SensorEvent) { + val rotationMatrix = FloatArray(9) + SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values) + val orientation = FloatArray(3) + SensorManager.getOrientation(rotationMatrix, orientation) + latestDirectionRad = orientation[0] // azimuth (radian) + } + + private fun emitCombinedImuData(speed: Float, acceleration: Float, directionRad: Float) { + onImuUpdate( + ImuData( + accelX = lastAccel[0], + accelY = lastAccel[1], + accelZ = lastAccel[2], + gyroX = lastGyro[0], + gyroY = lastGyro[1], + gyroZ = lastGyro[2], + speed = speed, + acceleration = acceleration, + directionRad = directionRad + ) + ) + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not used + } +} + +data class ImuData( + val accelX: Float, + val accelY: Float, + val accelZ: Float, + val gyroX: Float, + val gyroY: Float, + val gyroZ: Float, + val speed: Float, + val acceleration: Float, + val directionRad: Float +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614150524.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614150524.kt new file mode 100644 index 0000000..61faf90 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager_20250614150524.kt @@ -0,0 +1,144 @@ +package com.dumon.plugin.geolocation.imu + +import android.content.Context +import android.hardware.* +import android.util.Log +import kotlin.math.* + +class ImuSensorManager( + private val context: Context, + private val onImuUpdate: (ImuData) -> Unit +) : SensorEventListener { + + private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION) + private val gyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) + private val rotationVector = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + + private var lastAccel = FloatArray(3) { 0f } + private var lastGyro = FloatArray(3) { 0f } + private var velocity = FloatArray(3) { 0f } + private var lastAccelTimestamp: Long = 0L + + private val accelLowPass = FloatArray(3) { 0f } + private val alpha = 0.85f + private val accelerationThreshold = 0.12f + private val idleTimeoutNs = 2_000_000_000L // 2 seconds + private var lastActiveTimestamp: Long = 0L + + private var latestDirectionRad = 0f + private var latestSpeed = 0f + + fun start() { + accelerometer?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } + gyroscope?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } + rotationVector?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } + Log.d("IMU_SENSOR", "IMU sensor tracking started") + } + + fun stop() { + sensorManager.unregisterListener(this) + Log.d("IMU_SENSOR", "IMU sensor tracking stopped") + } + + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + when (it.sensor.type) { + Sensor.TYPE_LINEAR_ACCELERATION -> handleAccelerometer(it) + Sensor.TYPE_GYROSCOPE -> handleGyroscope(it) + Sensor.TYPE_ROTATION_VECTOR -> handleRotationVector(it) + } + } + } + + private fun handleAccelerometer(event: SensorEvent) { + val currentTime = event.timestamp + + for (i in 0..2) { + accelLowPass[i] = alpha * accelLowPass[i] + (1 - alpha) * event.values[i] + } + + val accMag = sqrt(accelLowPass.map { it * it }.sum()) + + if (accMag > accelerationThreshold) { + if (lastAccelTimestamp != 0L) { + val dt = (currentTime - lastAccelTimestamp) / 1_000_000_000f + for (i in 0..2) { + velocity[i] += accelLowPass[i] * dt + } + } + lastActiveTimestamp = currentTime + } else { + if (currentTime - lastActiveTimestamp > idleTimeoutNs) { + for (i in 0..2) velocity[i] = 0f + } else { + for (i in 0..2) velocity[i] *= 0.96f + } + } + + lastAccelTimestamp = currentTime + lastAccel = accelLowPass.copyOf() + + val speed = velocity.map { it * it }.sum().pow(0.5f).coerceIn(0f, 30f) + val acceleration = accMag.coerceIn(0f, 20f) + + latestSpeed = speed + + emitCombinedImuData(speed, acceleration, latestDirectionRad) + + Log.d("IMU_SENSOR", "Accel x: %.3f y: %.3f z: %.3f | Speed: %.3f | AccelMag: %.3f | Dir: %.2f rad".format( + lastAccel[0], lastAccel[1], lastAccel[2], speed, acceleration, latestDirectionRad + )) + } + + private fun handleGyroscope(event: SensorEvent) { + lastGyro = event.values.copyOf() + } + + private fun handleRotationVector(event: SensorEvent) { + val rotationMatrix = FloatArray(9) + SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values) + val orientation = FloatArray(3) + SensorManager.getOrientation(rotationMatrix, orientation) + + val azimuth = orientation[0] // radian + + // Apply smoothing to direction + val smoothFactor = 0.85f + if (abs(azimuth - latestDirectionRad) > 0.05f) { + latestDirectionRad = smoothFactor * latestDirectionRad + (1 - smoothFactor) * azimuth + } + } + + private fun emitCombinedImuData(speed: Float, acceleration: Float, directionRad: Float) { + onImuUpdate( + ImuData( + accelX = lastAccel[0], + accelY = lastAccel[1], + accelZ = lastAccel[2], + gyroX = lastGyro[0], + gyroY = lastGyro[1], + gyroZ = lastGyro[2], + speed = speed, + acceleration = acceleration, + directionRad = directionRad + ) + ) + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { + // Not used + } +} + +data class ImuData( + val accelX: Float, + val accelY: Float, + val accelZ: Float, + val gyroX: Float, + val gyroY: Float, + val gyroZ: Float, + val speed: Float, + val acceleration: Float, + val directionRad: Float +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614101730.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614101730.kt new file mode 100644 index 0000000..79b80af --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614101730.kt @@ -0,0 +1,123 @@ +package com.dumon.plugin.geolocation.wifi + +import android.content.Context +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.net.wifi.rtt.RangingRequest +import android.net.wifi.rtt.RangingResultCallback +import android.net.wifi.rtt.WifiRttManager +import android.os.* +import android.util.Log +import java.util.concurrent.Executors + +class WifiPositioningManager( + private val context: Context, + private val onWifiPositioningUpdate: (String) -> Unit = {} +) { + + private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val wifiRttManager: WifiRttManager? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.applicationContext.getSystemService(Context.WIFI_RTT_RANGING_SERVICE) as? WifiRttManager + } else { + null + } + + private val handler = Handler(Looper.getMainLooper()) + private var isScanning = false + + fun isRttSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && wifiRttManager != null && wifiManager.isWifiEnabled + } + + fun startPeriodicScan(intervalMs: Long = 3000L) { + isScanning = true + handler.post(object : Runnable { + override fun run() { + if (isScanning) { + startWifiScan() + handler.postDelayed(this, intervalMs) + } + } + }) + } + + fun stopPeriodicScan() { + isScanning = false + handler.removeCallbacksAndMessages(null) + } + + fun startWifiScan() { + val success = wifiManager.startScan() + if (success) { + val results = wifiManager.scanResults + logScanResults(results) + + if (isRttSupported()) { + startRttRanging(results) + } + } else { + Log.e("WIFI_POSITION", "Wi-Fi scan failed") + onWifiPositioningUpdate("Wi-Fi scan failed") + } + } + + private fun logScanResults(results: List) { + val info = buildString { + append("Wi-Fi AP count: ${results.size}\n") + results.forEach { result -> + append("SSID: ${result.SSID}, BSSID: ${result.BSSID}, RSSI: ${result.level} dBm\n") + } + } + Log.d("WIFI_POSITION", info) + onWifiPositioningUpdate(info) + } + + private fun startRttRanging(results: List) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || wifiRttManager == null) { + Log.d("WIFI_POSITION", "RTT not supported or WifiRttManager is null.") + return + } + + val rttCapableAps = results.filter { it.is80211mcResponder } + + if (rttCapableAps.isEmpty()) { + Log.d("WIFI_POSITION", "No RTT-capable AP found.") + return + } + + val rangingRequest = RangingRequest.Builder() + .addAccessPoints(rttCapableAps) + .build() + + try { + wifiRttManager.startRanging( + rangingRequest, + Executors.newSingleThreadExecutor(), + object : RangingResultCallback() { + override fun onRangingFailure(code: Int) { + Log.e("WIFI_POSITION", "RTT Ranging failed: $code") + } + + override fun onRangingResults(results: List) { + results.forEach { result -> + if (result.status == android.net.wifi.rtt.RangingResult.STATUS_SUCCESS) { + Log.d( + "WIFI_POSITION", + "RTT Result โ†’ BSSID: ${result.macAddress}, Distance: ${result.distanceMm / 1000.0} m" + ) + } else { + Log.d( + "WIFI_POSITION", + "RTT Result โ†’ BSSID: ${result.macAddress}, Status: ${result.status}" + ) + } + } + } + } + ) + } catch (e: SecurityException) { + Log.e("WIFI_POSITION", "SecurityException: Missing NEARBY_WIFI_DEVICES permission or not granted", e) + } + } +} \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614113056.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614113056.kt new file mode 100644 index 0000000..af3201b --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614113056.kt @@ -0,0 +1,151 @@ +package com.dumon.plugin.geolocation.wifi + +import android.content.Context +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.net.wifi.rtt.RangingRequest +import android.net.wifi.rtt.RangingResultCallback +import android.net.wifi.rtt.WifiRttManager +import android.os.* +import android.util.Log +import java.util.concurrent.Executors + +class WifiPositioningManager( + private val context: Context, + private val onWifiPositioningUpdate: (WifiScanResult) -> Unit +) { + + private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val wifiRttManager: WifiRttManager? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.applicationContext.getSystemService(Context.WIFI_RTT_RANGING_SERVICE) as? WifiRttManager + } else { + null + } + + private val handler = Handler(Looper.getMainLooper()) + private var isScanning = false + + fun isRttSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && wifiRttManager != null && wifiManager.isWifiEnabled + } + + fun startPeriodicScan(intervalMs: Long = 3000L) { + isScanning = true + handler.post(object : Runnable { + override fun run() { + if (isScanning) { + startWifiScan() + handler.postDelayed(this, intervalMs) + } + } + }) + } + + fun stopPeriodicScan() { + isScanning = false + handler.removeCallbacksAndMessages(null) + } + + fun startWifiScan() { + val success = wifiManager.startScan() + if (success) { + val results = wifiManager.scanResults + logScanResults(results) + + if (isRttSupported()) { + startRttRanging(results) + } + } else { + onWifiPositioningUpdate( + WifiScanResult( + apCount = 0, + aps = emptyList() + ) + ) + } + } + + private fun logScanResults(results: List) { + val aps = results.map { result -> + WifiAp( + ssid = result.SSID, + bssid = result.BSSID, + rssi = result.level + // distance belum ada di ScanResult, hanya di RTT โ†’ akan diupdate di startRttRanging() + ) + } + + val wifiScanResult = WifiScanResult( + apCount = aps.size, + aps = aps + ) + + Log.d("WIFI_POSITION", "Wi-Fi scan โ†’ AP count: ${aps.size}") + aps.forEach { + Log.d("WIFI_POSITION", "SSID: ${it.ssid}, BSSID: ${it.bssid}, RSSI: ${it.rssi} dBm") + } + + onWifiPositioningUpdate(wifiScanResult) + } + + private fun startRttRanging(results: List) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || wifiRttManager == null) { + Log.d("WIFI_POSITION", "RTT not supported or WifiRttManager is null.") + return + } + + val rttCapableAps = results.filter { it.is80211mcResponder } + + if (rttCapableAps.isEmpty()) { + Log.d("WIFI_POSITION", "No RTT-capable AP found.") + return + } + + val rangingRequest = RangingRequest.Builder() + .addAccessPoints(rttCapableAps) + .build() + + try { + wifiRttManager.startRanging( + rangingRequest, + Executors.newSingleThreadExecutor(), + object : RangingResultCallback() { + override fun onRangingFailure(code: Int) { + Log.e("WIFI_POSITION", "RTT Ranging failed: $code") + } + + override fun onRangingResults(results: List) { + results.forEach { result -> + if (result.status == android.net.wifi.rtt.RangingResult.STATUS_SUCCESS) { + Log.d( + "WIFI_POSITION", + "RTT Result โ†’ BSSID: ${result.macAddress}, Distance: ${result.distanceMm / 1000.0} m" + ) + } else { + Log.d( + "WIFI_POSITION", + "RTT Result โ†’ BSSID: ${result.macAddress}, Status: ${result.status}" + ) + } + } + } + } + ) + } catch (e: SecurityException) { + Log.e("WIFI_POSITION", "SecurityException: Missing NEARBY_WIFI_DEVICES permission or not granted", e) + } + } +} + +data class WifiAp( + val ssid: String, + val bssid: String, + val rssi: Int, + val distance: Double? = null +) + +data class WifiScanResult( + val apCount: Int, + val aps: List +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614113218.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614113218.kt new file mode 100644 index 0000000..36f8811 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614113218.kt @@ -0,0 +1,148 @@ +package com.dumon.plugin.geolocation.wifi + +import android.content.Context +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.net.wifi.rtt.RangingRequest +import android.net.wifi.rtt.RangingResultCallback +import android.net.wifi.rtt.WifiRttManager +import android.os.* +import android.util.Log +import java.util.concurrent.Executors + +class WifiPositioningManager( + private val context: Context, + private val onWifiPositioningUpdate: (WifiScanResult) -> Unit +) { + + private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val wifiRttManager: WifiRttManager? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.applicationContext.getSystemService(Context.WIFI_RTT_RANGING_SERVICE) as? WifiRttManager + } else { + null + } + + private val handler = Handler(Looper.getMainLooper()) + private var isScanning = false + + private var lastWifiScanAps: MutableList = mutableListOf() + + fun isRttSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && wifiRttManager != null && wifiManager.isWifiEnabled + } + + fun startPeriodicScan(intervalMs: Long = 3000L) { + isScanning = true + handler.post(object : Runnable { + override fun run() { + if (isScanning) { + startWifiScan() + handler.postDelayed(this, intervalMs) + } + } + }) + } + + fun stopPeriodicScan() { + isScanning = false + handler.removeCallbacksAndMessages(null) + } + + fun startWifiScan() { + val success = wifiManager.startScan() + if (success) { + val results = wifiManager.scanResults + processScanResults(results) + if (isRttSupported()) { + startRttRanging(results) + } + } else { + Log.e("WIFI_POSITION", "Wi-Fi scan failed") + onWifiPositioningUpdate(WifiScanResult(0, emptyList())) + } + } + + private fun processScanResults(results: List) { + lastWifiScanAps = results.map { result -> + WifiAp( + ssid = result.SSID, + bssid = result.BSSID, + rssi = result.level + ) + }.toMutableList() + + Log.d("WIFI_POSITION", "Wi-Fi scan โ†’ AP count: ${lastWifiScanAps.size}") + lastWifiScanAps.forEach { + Log.d("WIFI_POSITION", "SSID: ${it.ssid}, BSSID: ${it.bssid}, RSSI: ${it.rssi} dBm") + } + + onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps)) + } + + private fun startRttRanging(results: List) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || wifiRttManager == null) { + Log.d("WIFI_POSITION", "RTT not supported or WifiRttManager is null.") + return + } + + val rttCapableAps = results.filter { it.is80211mcResponder } + if (rttCapableAps.isEmpty()) { + Log.d("WIFI_POSITION", "No RTT-capable AP found.") + return + } + + val rangingRequest = RangingRequest.Builder() + .addAccessPoints(rttCapableAps) + .build() + + try { + wifiRttManager.startRanging( + rangingRequest, + Executors.newSingleThreadExecutor(), + object : RangingResultCallback() { + override fun onRangingFailure(code: Int) { + Log.e("WIFI_POSITION", "RTT Ranging failed: $code") + } + + override fun onRangingResults(results: List) { + results.forEach { result -> + val mac = result.macAddress.toString() + val distance = if (result.status == android.net.wifi.rtt.RangingResult.STATUS_SUCCESS) { + result.distanceMm / 1000.0 + } else null + + lastWifiScanAps.indexOfFirst { it.bssid == mac }.takeIf { it >= 0 }?.let { idx -> + lastWifiScanAps[idx] = lastWifiScanAps[idx].copy(distance = distance) + } + + Log.d( + "WIFI_POSITION", + if (distance != null) + "RTT โ†’ ${mac}, Distance: ${distance} m" + else + "RTT โ†’ ${mac}, Status: ${result.status}" + ) + } + + onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps)) + } + } + ) + } catch (e: SecurityException) { + Log.e("WIFI_POSITION", "SecurityException: Missing NEARBY_WIFI_DEVICES permission", e) + } + } +} + +data class WifiAp( + val ssid: String, + val bssid: String, + val rssi: Int, + val distance: Double? = null +) + +data class WifiScanResult( + val apCount: Int, + val aps: List +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614120124.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614120124.kt new file mode 100644 index 0000000..b43017f --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614120124.kt @@ -0,0 +1,142 @@ +package com.dumon.plugin.geolocation.wifi + +import android.content.Context +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.net.wifi.rtt.RangingRequest +import android.net.wifi.rtt.RangingResultCallback +import android.net.wifi.rtt.WifiRttManager +import android.os.* +import android.util.Log +import java.util.concurrent.Executors + +class WifiPositioningManager( + private val context: Context, + private val onWifiPositioningUpdate: (WifiScanResult) -> Unit +) { + private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val wifiRttManager: WifiRttManager? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.applicationContext.getSystemService(Context.WIFI_RTT_RANGING_SERVICE) as? WifiRttManager + } else null + + private val handler = Handler(Looper.getMainLooper()) + private var isScanning = false + private var lastScanAps: MutableList = mutableListOf() + + fun isRttSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && wifiRttManager != null && wifiManager.isWifiEnabled + } + + fun startPeriodicScan(intervalMs: Long = 3000L) { + isScanning = true + handler.post(object : Runnable { + override fun run() { + if (isScanning) { + startWifiScan() + handler.postDelayed(this, intervalMs) + } + } + }) + } + + fun stopPeriodicScan() { + isScanning = false + handler.removeCallbacksAndMessages(null) + } + + fun startWifiScan() { + val success = wifiManager.startScan() + if (success) { + val results = wifiManager.scanResults + processScanResults(results) + if (isRttSupported()) { + startRttRanging(results) + } + } else { + Log.e("WIFI_POSITION", "Wi-Fi scan failed") + onWifiPositioningUpdate(WifiScanResult(0, emptyList())) + } + } + + private fun processScanResults(results: List) { + lastScanAps = results.map { result -> + WifiAp( + ssid = result.SSID, + bssid = result.BSSID, + rssi = result.level + ) + }.toMutableList() + + Log.d("WIFI_POSITION", "Scan result โ†’ ${lastScanAps.size} APs") + lastScanAps.forEach { + Log.d("WIFI_AP", "SSID=${it.ssid}, BSSID=${it.bssid}, RSSI=${it.rssi} dBm") + } + + onWifiPositioningUpdate(WifiScanResult(lastScanAps.size, lastScanAps)) + } + + private fun startRttRanging(results: List) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || wifiRttManager == null) return + + val rttCapable = results.filter { it.is80211mcResponder } + if (rttCapable.isEmpty()) { + Log.d("WIFI_RTT", "No RTT-capable AP found.") + return + } + + val request = RangingRequest.Builder() + .addAccessPoints(rttCapable) + .build() + + try { + wifiRttManager.startRanging( + request, + Executors.newSingleThreadExecutor(), + object : RangingResultCallback() { + override fun onRangingFailure(code: Int) { + Log.e("WIFI_RTT", "Ranging failed: $code") + } + + override fun onRangingResults(results: List) { + results.forEach { result -> + val bssid = result.macAddress.toString() + val distance = if (result.status == android.net.wifi.rtt.RangingResult.STATUS_SUCCESS) { + result.distanceMm / 1000.0 + } else null + + lastScanAps.indexOfFirst { it.bssid == bssid } + .takeIf { it >= 0 }?.let { idx -> + lastScanAps[idx] = lastScanAps[idx].copy(distance = distance) + } + + Log.d( + "WIFI_RTT", + if (distance != null) + "RTT โ†’ $bssid : ${distance} m" + else + "RTT โ†’ $bssid : FAILED" + ) + } + + onWifiPositioningUpdate(WifiScanResult(lastScanAps.size, lastScanAps)) + } + } + ) + } catch (e: SecurityException) { + Log.e("WIFI_RTT", "Missing NEARBY_WIFI_DEVICES permission", e) + } + } +} + +data class WifiAp( + val ssid: String, + val bssid: String, + val rssi: Int, + val distance: Double? = null +) + +data class WifiScanResult( + val apCount: Int, + val aps: List +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614123055.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614123055.kt new file mode 100644 index 0000000..bb0b261 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614123055.kt @@ -0,0 +1,163 @@ +package com.dumon.plugin.geolocation.wifi + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.net.wifi.rtt.RangingRequest +import android.net.wifi.rtt.RangingResultCallback +import android.net.wifi.rtt.WifiRttManager +import android.os.* +import android.util.Log +import androidx.core.app.ActivityCompat +import java.util.concurrent.Executors + +class WifiPositioningManager( + private val context: Context, + private val onWifiPositioningUpdate: (WifiScanResult) -> Unit +) { + + private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val wifiRttManager: WifiRttManager? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.applicationContext.getSystemService(Context.WIFI_RTT_RANGING_SERVICE) as? WifiRttManager + } else { + null + } + + private val handler = Handler(Looper.getMainLooper()) + private var isScanning = false + + private var lastWifiScanAps: MutableList = mutableListOf() + + fun isRttSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && wifiRttManager != null && wifiManager.isWifiEnabled + } + + fun startPeriodicScan(intervalMs: Long = 3000L) { + isScanning = true + handler.post(object : Runnable { + override fun run() { + if (isScanning) { + startWifiScan() + handler.postDelayed(this, intervalMs) + } + } + }) + } + + fun stopPeriodicScan() { + isScanning = false + handler.removeCallbacksAndMessages(null) + } + + fun startWifiScan() { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + val success = wifiManager.startScan() + if (success) { + val results = wifiManager.scanResults + processScanResults(results) + if (isRttSupported()) { + startRttRanging(results) + } + } else { + Log.e("WIFI_POSITION", "Wi-Fi scan failed") + onWifiPositioningUpdate(WifiScanResult(0, emptyList())) + } + } else { + Log.e("WIFI_POSITION", "Missing ACCESS_FINE_LOCATION permission") + onWifiPositioningUpdate(WifiScanResult(0, emptyList())) + } + } + + private fun processScanResults(results: List) { + lastWifiScanAps = results.map { result -> + WifiAp( + ssid = result.SSID, + bssid = result.BSSID, + rssi = result.level + ) + }.toMutableList() + + Log.d("WIFI_POSITION", "Wi-Fi scan โ†’ AP count: ${lastWifiScanAps.size}") + lastWifiScanAps.forEach { + Log.d("WIFI_POSITION", "SSID: ${it.ssid}, BSSID: ${it.bssid}, RSSI: ${it.rssi} dBm") + } + + onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps)) + } + + private fun startRttRanging(results: List) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || wifiRttManager == null) { + Log.d("WIFI_POSITION", "RTT not supported or WifiRttManager is null.") + return + } + + val rttCapableAps = results.filter { it.is80211mcResponder } + if (rttCapableAps.isEmpty()) { + Log.d("WIFI_POSITION", "No RTT-capable AP found.") + return + } + + val rangingRequest = RangingRequest.Builder() + .addAccessPoints(rttCapableAps) + .build() + + try { + wifiRttManager.startRanging( + rangingRequest, + Executors.newSingleThreadExecutor(), + object : RangingResultCallback() { + override fun onRangingFailure(code: Int) { + Log.e("WIFI_POSITION", "RTT Ranging failed: $code") + } + + override fun onRangingResults(results: List) { + results.forEach { result -> + val mac = result.macAddress.toString() + val distance = if (result.status == android.net.wifi.rtt.RangingResult.STATUS_SUCCESS) { + result.distanceMm / 1000.0 + } else { + Log.w("WIFI_POSITION", "RTT distance unavailable for ${result.macAddress}") + null + } + + lastWifiScanAps.indexOfFirst { it.bssid == mac }.takeIf { it >= 0 }?.let { idx -> + lastWifiScanAps[idx] = lastWifiScanAps[idx].copy(distance = distance) + } + + Log.d( + "WIFI_POSITION", + if (distance != null) + "RTT โ†’ ${mac}, Distance: ${distance} m" + else + "RTT โ†’ ${mac}, Status: ${result.status}" + ) + } + + onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps)) + } + } + ) + } catch (e: SecurityException) { + Log.e("WIFI_POSITION", "SecurityException: Missing NEARBY_WIFI_DEVICES permission", e) + } + } +} + +data class WifiAp( + val ssid: String, + val bssid: String, + val rssi: Int, + val distance: Double? = null +) + +data class WifiScanResult( + val apCount: Int, + val aps: List +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614123056.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614123056.kt new file mode 100644 index 0000000..bb0b261 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614123056.kt @@ -0,0 +1,163 @@ +package com.dumon.plugin.geolocation.wifi + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.net.wifi.rtt.RangingRequest +import android.net.wifi.rtt.RangingResultCallback +import android.net.wifi.rtt.WifiRttManager +import android.os.* +import android.util.Log +import androidx.core.app.ActivityCompat +import java.util.concurrent.Executors + +class WifiPositioningManager( + private val context: Context, + private val onWifiPositioningUpdate: (WifiScanResult) -> Unit +) { + + private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val wifiRttManager: WifiRttManager? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.applicationContext.getSystemService(Context.WIFI_RTT_RANGING_SERVICE) as? WifiRttManager + } else { + null + } + + private val handler = Handler(Looper.getMainLooper()) + private var isScanning = false + + private var lastWifiScanAps: MutableList = mutableListOf() + + fun isRttSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && wifiRttManager != null && wifiManager.isWifiEnabled + } + + fun startPeriodicScan(intervalMs: Long = 3000L) { + isScanning = true + handler.post(object : Runnable { + override fun run() { + if (isScanning) { + startWifiScan() + handler.postDelayed(this, intervalMs) + } + } + }) + } + + fun stopPeriodicScan() { + isScanning = false + handler.removeCallbacksAndMessages(null) + } + + fun startWifiScan() { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + val success = wifiManager.startScan() + if (success) { + val results = wifiManager.scanResults + processScanResults(results) + if (isRttSupported()) { + startRttRanging(results) + } + } else { + Log.e("WIFI_POSITION", "Wi-Fi scan failed") + onWifiPositioningUpdate(WifiScanResult(0, emptyList())) + } + } else { + Log.e("WIFI_POSITION", "Missing ACCESS_FINE_LOCATION permission") + onWifiPositioningUpdate(WifiScanResult(0, emptyList())) + } + } + + private fun processScanResults(results: List) { + lastWifiScanAps = results.map { result -> + WifiAp( + ssid = result.SSID, + bssid = result.BSSID, + rssi = result.level + ) + }.toMutableList() + + Log.d("WIFI_POSITION", "Wi-Fi scan โ†’ AP count: ${lastWifiScanAps.size}") + lastWifiScanAps.forEach { + Log.d("WIFI_POSITION", "SSID: ${it.ssid}, BSSID: ${it.bssid}, RSSI: ${it.rssi} dBm") + } + + onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps)) + } + + private fun startRttRanging(results: List) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || wifiRttManager == null) { + Log.d("WIFI_POSITION", "RTT not supported or WifiRttManager is null.") + return + } + + val rttCapableAps = results.filter { it.is80211mcResponder } + if (rttCapableAps.isEmpty()) { + Log.d("WIFI_POSITION", "No RTT-capable AP found.") + return + } + + val rangingRequest = RangingRequest.Builder() + .addAccessPoints(rttCapableAps) + .build() + + try { + wifiRttManager.startRanging( + rangingRequest, + Executors.newSingleThreadExecutor(), + object : RangingResultCallback() { + override fun onRangingFailure(code: Int) { + Log.e("WIFI_POSITION", "RTT Ranging failed: $code") + } + + override fun onRangingResults(results: List) { + results.forEach { result -> + val mac = result.macAddress.toString() + val distance = if (result.status == android.net.wifi.rtt.RangingResult.STATUS_SUCCESS) { + result.distanceMm / 1000.0 + } else { + Log.w("WIFI_POSITION", "RTT distance unavailable for ${result.macAddress}") + null + } + + lastWifiScanAps.indexOfFirst { it.bssid == mac }.takeIf { it >= 0 }?.let { idx -> + lastWifiScanAps[idx] = lastWifiScanAps[idx].copy(distance = distance) + } + + Log.d( + "WIFI_POSITION", + if (distance != null) + "RTT โ†’ ${mac}, Distance: ${distance} m" + else + "RTT โ†’ ${mac}, Status: ${result.status}" + ) + } + + onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps)) + } + } + ) + } catch (e: SecurityException) { + Log.e("WIFI_POSITION", "SecurityException: Missing NEARBY_WIFI_DEVICES permission", e) + } + } +} + +data class WifiAp( + val ssid: String, + val bssid: String, + val rssi: Int, + val distance: Double? = null +) + +data class WifiScanResult( + val apCount: Int, + val aps: List +) \ No newline at end of file diff --git a/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614135351.kt b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614135351.kt new file mode 100644 index 0000000..bb0b261 --- /dev/null +++ b/.history/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager_20250614135351.kt @@ -0,0 +1,163 @@ +package com.dumon.plugin.geolocation.wifi + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.net.wifi.ScanResult +import android.net.wifi.WifiManager +import android.net.wifi.rtt.RangingRequest +import android.net.wifi.rtt.RangingResultCallback +import android.net.wifi.rtt.WifiRttManager +import android.os.* +import android.util.Log +import androidx.core.app.ActivityCompat +import java.util.concurrent.Executors + +class WifiPositioningManager( + private val context: Context, + private val onWifiPositioningUpdate: (WifiScanResult) -> Unit +) { + + private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager + private val wifiRttManager: WifiRttManager? = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + context.applicationContext.getSystemService(Context.WIFI_RTT_RANGING_SERVICE) as? WifiRttManager + } else { + null + } + + private val handler = Handler(Looper.getMainLooper()) + private var isScanning = false + + private var lastWifiScanAps: MutableList = mutableListOf() + + fun isRttSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && wifiRttManager != null && wifiManager.isWifiEnabled + } + + fun startPeriodicScan(intervalMs: Long = 3000L) { + isScanning = true + handler.post(object : Runnable { + override fun run() { + if (isScanning) { + startWifiScan() + handler.postDelayed(this, intervalMs) + } + } + }) + } + + fun stopPeriodicScan() { + isScanning = false + handler.removeCallbacksAndMessages(null) + } + + fun startWifiScan() { + if (ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + ) { + val success = wifiManager.startScan() + if (success) { + val results = wifiManager.scanResults + processScanResults(results) + if (isRttSupported()) { + startRttRanging(results) + } + } else { + Log.e("WIFI_POSITION", "Wi-Fi scan failed") + onWifiPositioningUpdate(WifiScanResult(0, emptyList())) + } + } else { + Log.e("WIFI_POSITION", "Missing ACCESS_FINE_LOCATION permission") + onWifiPositioningUpdate(WifiScanResult(0, emptyList())) + } + } + + private fun processScanResults(results: List) { + lastWifiScanAps = results.map { result -> + WifiAp( + ssid = result.SSID, + bssid = result.BSSID, + rssi = result.level + ) + }.toMutableList() + + Log.d("WIFI_POSITION", "Wi-Fi scan โ†’ AP count: ${lastWifiScanAps.size}") + lastWifiScanAps.forEach { + Log.d("WIFI_POSITION", "SSID: ${it.ssid}, BSSID: ${it.bssid}, RSSI: ${it.rssi} dBm") + } + + onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps)) + } + + private fun startRttRanging(results: List) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || wifiRttManager == null) { + Log.d("WIFI_POSITION", "RTT not supported or WifiRttManager is null.") + return + } + + val rttCapableAps = results.filter { it.is80211mcResponder } + if (rttCapableAps.isEmpty()) { + Log.d("WIFI_POSITION", "No RTT-capable AP found.") + return + } + + val rangingRequest = RangingRequest.Builder() + .addAccessPoints(rttCapableAps) + .build() + + try { + wifiRttManager.startRanging( + rangingRequest, + Executors.newSingleThreadExecutor(), + object : RangingResultCallback() { + override fun onRangingFailure(code: Int) { + Log.e("WIFI_POSITION", "RTT Ranging failed: $code") + } + + override fun onRangingResults(results: List) { + results.forEach { result -> + val mac = result.macAddress.toString() + val distance = if (result.status == android.net.wifi.rtt.RangingResult.STATUS_SUCCESS) { + result.distanceMm / 1000.0 + } else { + Log.w("WIFI_POSITION", "RTT distance unavailable for ${result.macAddress}") + null + } + + lastWifiScanAps.indexOfFirst { it.bssid == mac }.takeIf { it >= 0 }?.let { idx -> + lastWifiScanAps[idx] = lastWifiScanAps[idx].copy(distance = distance) + } + + Log.d( + "WIFI_POSITION", + if (distance != null) + "RTT โ†’ ${mac}, Distance: ${distance} m" + else + "RTT โ†’ ${mac}, Status: ${result.status}" + ) + } + + onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps)) + } + } + ) + } catch (e: SecurityException) { + Log.e("WIFI_POSITION", "SecurityException: Missing NEARBY_WIFI_DEVICES permission", e) + } + } +} + +data class WifiAp( + val ssid: String, + val bssid: String, + val rssi: Int, + val distance: Double? = null +) + +data class WifiScanResult( + val apCount: Int, + val aps: List +) \ No newline at end of file diff --git a/.history/example-app/android/app/src/main/AndroidManifest_20250605213534.xml b/.history/example-app/android/app/src/main/AndroidManifest_20250605213534.xml new file mode 100644 index 0000000..340e7df --- /dev/null +++ b/.history/example-app/android/app/src/main/AndroidManifest_20250605213534.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.history/example-app/android/app/src/main/AndroidManifest_20250614110026.xml b/.history/example-app/android/app/src/main/AndroidManifest_20250614110026.xml new file mode 100644 index 0000000..9d1f585 --- /dev/null +++ b/.history/example-app/android/app/src/main/AndroidManifest_20250614110026.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.history/example-app/src/index_20250115225119.html b/.history/example-app/src/index_20250115225119.html new file mode 100644 index 0000000..ae96d8d --- /dev/null +++ b/.history/example-app/src/index_20250115225119.html @@ -0,0 +1,26 @@ + + + + + Example Capacitor App + + + + +
+

Capacitor Test Plugin Project

+

+ This project can be used to test out the functionality of your plugin. Nothing in the + example-app/ folder will be published to npm when using this template, so you can create away! +

+ + + +
+ + + + diff --git a/.history/example-app/src/index_20250614103708.html b/.history/example-app/src/index_20250614103708.html new file mode 100644 index 0000000..2eaea6c --- /dev/null +++ b/.history/example-app/src/index_20250614103708.html @@ -0,0 +1,24 @@ + + + + + Example Capacitor App - DumonGeolocation + + + + +

Capacitor DumonGeolocation Plugin Test

+ +
+ + + + +

+
+    
+  
+
\ No newline at end of file
diff --git a/.history/example-app/src/index_20250614121429.html b/.history/example-app/src/index_20250614121429.html
new file mode 100644
index 0000000..bf83e55
--- /dev/null
+++ b/.history/example-app/src/index_20250614121429.html
@@ -0,0 +1,58 @@
+
+
+  
+    
+    Example Capacitor App - DumonGeolocation
+    
+    
+    
+  
+  
+    

Capacitor DumonGeolocation Plugin Test

+ + + + + +

+
+    
+  
+
\ No newline at end of file
diff --git a/.history/example-app/src/js/example_20250614100113.js b/.history/example-app/src/js/example_20250614100113.js
new file mode 100644
index 0000000..6940a32
--- /dev/null
+++ b/.history/example-app/src/js/example_20250614100113.js
@@ -0,0 +1,6 @@
+import { DumonGeolocation } from 'dumon-geolocation';
+
+window.testEcho = () => {
+    const inputValue = document.getElementById("echoInput").value;
+    DumonGeolocation.echo({ value: inputValue })
+}
diff --git a/.history/example-app/src/js/example_20250614103747.js b/.history/example-app/src/js/example_20250614103747.js
new file mode 100644
index 0000000..61bc127
--- /dev/null
+++ b/.history/example-app/src/js/example_20250614103747.js
@@ -0,0 +1,24 @@
+import { DumonGeolocation } from '@dumon/capacitor-geolocation';
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    console.log('[onPositionUpdate]', data);
+  });
+
+  await DumonGeolocation.startPositioning();
+}
+
+async function stopGeolocation() {
+  await DumonGeolocation.stopPositioning();
+}
+
+async function getLatestPosition() {
+  const data = await DumonGeolocation.getLatestPosition();
+  console.log('[getLatestPosition]', data);
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/.history/example-app/src/js/example_20250614105523.js b/.history/example-app/src/js/example_20250614105523.js
new file mode 100644
index 0000000..f49f527
--- /dev/null
+++ b/.history/example-app/src/js/example_20250614105523.js
@@ -0,0 +1,24 @@
+import { DumonGeolocation } from 'dumon-geolocation';
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    console.log('[onPositionUpdate]', data);
+  });
+
+  await DumonGeolocation.startPositioning();
+}
+
+async function stopGeolocation() {
+  await DumonGeolocation.stopPositioning();
+}
+
+async function getLatestPosition() {
+  const data = await DumonGeolocation.getLatestPosition();
+  console.log('[getLatestPosition]', data);
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/.history/example-app/src/js/example_20250614121455.js b/.history/example-app/src/js/example_20250614121455.js
new file mode 100644
index 0000000..c746446
--- /dev/null
+++ b/.history/example-app/src/js/example_20250614121455.js
@@ -0,0 +1,46 @@
+import { DumonGeolocation } from 'dumon-geolocation';
+
+const logArea = document.getElementById('logArea');
+
+function appendLog(title, data) {
+  const timestamp = new Date().toLocaleTimeString();
+  const formatted = `[${timestamp}] ${title}\n${JSON.stringify(data, null, 2)}\n\n`;
+  logArea.textContent = formatted + logArea.textContent;
+}
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    appendLog('onPositionUpdate', data);
+  });
+
+  try {
+    await DumonGeolocation.startPositioning();
+    appendLog('startPositioning', { success: true });
+  } catch (err) {
+    appendLog('startPositioning', { error: err.message });
+  }
+}
+
+async function stopGeolocation() {
+  try {
+    await DumonGeolocation.stopPositioning();
+    appendLog('stopPositioning', { success: true });
+  } catch (err) {
+    appendLog('stopPositioning', { error: err.message });
+  }
+}
+
+async function getLatestPosition() {
+  try {
+    const data = await DumonGeolocation.getLatestPosition();
+    appendLog('getLatestPosition', data);
+  } catch (err) {
+    appendLog('getLatestPosition', { error: err.message });
+  }
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/.history/example-app/src/js/example_20250614123723.js b/.history/example-app/src/js/example_20250614123723.js
new file mode 100644
index 0000000..04191f6
--- /dev/null
+++ b/.history/example-app/src/js/example_20250614123723.js
@@ -0,0 +1,46 @@
+import { DumonGeolocation } from 'dumon-geolocation';
+
+const logArea = document.getElementById('logArea');
+
+function appendLog(title, data) {
+  const timestamp = new Date().toLocaleTimeString();
+  const formatted = `[${timestamp}] ${title}\n${JSON.stringify(data, null, 2)}\n\n`;
+  logArea.textContent = formatted; // + logArea.textContent;
+}
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    appendLog('onPositionUpdate', data);
+  });
+
+  try {
+    await DumonGeolocation.startPositioning();
+    appendLog('startPositioning', { success: true });
+  } catch (err) {
+    appendLog('startPositioning', { error: err.message });
+  }
+}
+
+async function stopGeolocation() {
+  try {
+    await DumonGeolocation.stopPositioning();
+    appendLog('stopPositioning', { success: true });
+  } catch (err) {
+    appendLog('stopPositioning', { error: err.message });
+  }
+}
+
+async function getLatestPosition() {
+  try {
+    const data = await DumonGeolocation.getLatestPosition();
+    appendLog('getLatestPosition', data);
+  } catch (err) {
+    appendLog('getLatestPosition', { error: err.message });
+  }
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/.history/example-app/src/js/example_20250614123846.js b/.history/example-app/src/js/example_20250614123846.js
new file mode 100644
index 0000000..af43a19
--- /dev/null
+++ b/.history/example-app/src/js/example_20250614123846.js
@@ -0,0 +1,49 @@
+import { DumonGeolocation } from 'dumon-geolocation';
+
+const logArea = document.getElementById('logArea');
+
+function appendLog(title, data) {
+  const timestamp = new Date().toLocaleTimeString();
+  const formatted = `[${timestamp}] ${title}\n${JSON.stringify(data, null, 2)}\n\n`;
+  if(data.wifiData.aps && data.wifiData.aps.length > 0){
+    console.log('---WiFi-data detected');
+  }
+  logArea.textContent = formatted; // + logArea.textContent;
+}
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    appendLog('onPositionUpdate', data);
+  });
+
+  try {
+    await DumonGeolocation.startPositioning();
+    appendLog('startPositioning', { success: true });
+  } catch (err) {
+    appendLog('startPositioning', { error: err.message });
+  }
+}
+
+async function stopGeolocation() {
+  try {
+    await DumonGeolocation.stopPositioning();
+    appendLog('stopPositioning', { success: true });
+  } catch (err) {
+    appendLog('stopPositioning', { error: err.message });
+  }
+}
+
+async function getLatestPosition() {
+  try {
+    const data = await DumonGeolocation.getLatestPosition();
+    appendLog('getLatestPosition', data);
+  } catch (err) {
+    appendLog('getLatestPosition', { error: err.message });
+  }
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/.history/example-app/src/js/example_20250614152308.js b/.history/example-app/src/js/example_20250614152308.js
new file mode 100644
index 0000000..04191f6
--- /dev/null
+++ b/.history/example-app/src/js/example_20250614152308.js
@@ -0,0 +1,46 @@
+import { DumonGeolocation } from 'dumon-geolocation';
+
+const logArea = document.getElementById('logArea');
+
+function appendLog(title, data) {
+  const timestamp = new Date().toLocaleTimeString();
+  const formatted = `[${timestamp}] ${title}\n${JSON.stringify(data, null, 2)}\n\n`;
+  logArea.textContent = formatted; // + logArea.textContent;
+}
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    appendLog('onPositionUpdate', data);
+  });
+
+  try {
+    await DumonGeolocation.startPositioning();
+    appendLog('startPositioning', { success: true });
+  } catch (err) {
+    appendLog('startPositioning', { error: err.message });
+  }
+}
+
+async function stopGeolocation() {
+  try {
+    await DumonGeolocation.stopPositioning();
+    appendLog('stopPositioning', { success: true });
+  } catch (err) {
+    appendLog('stopPositioning', { error: err.message });
+  }
+}
+
+async function getLatestPosition() {
+  try {
+    const data = await DumonGeolocation.getLatestPosition();
+    appendLog('getLatestPosition', data);
+  } catch (err) {
+    appendLog('getLatestPosition', { error: err.message });
+  }
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/.history/example-app/src/js/example_20250614152312.js b/.history/example-app/src/js/example_20250614152312.js
new file mode 100644
index 0000000..04191f6
--- /dev/null
+++ b/.history/example-app/src/js/example_20250614152312.js
@@ -0,0 +1,46 @@
+import { DumonGeolocation } from 'dumon-geolocation';
+
+const logArea = document.getElementById('logArea');
+
+function appendLog(title, data) {
+  const timestamp = new Date().toLocaleTimeString();
+  const formatted = `[${timestamp}] ${title}\n${JSON.stringify(data, null, 2)}\n\n`;
+  logArea.textContent = formatted; // + logArea.textContent;
+}
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    appendLog('onPositionUpdate', data);
+  });
+
+  try {
+    await DumonGeolocation.startPositioning();
+    appendLog('startPositioning', { success: true });
+  } catch (err) {
+    appendLog('startPositioning', { error: err.message });
+  }
+}
+
+async function stopGeolocation() {
+  try {
+    await DumonGeolocation.stopPositioning();
+    appendLog('stopPositioning', { success: true });
+  } catch (err) {
+    appendLog('stopPositioning', { error: err.message });
+  }
+}
+
+async function getLatestPosition() {
+  try {
+    const data = await DumonGeolocation.getLatestPosition();
+    appendLog('getLatestPosition', data);
+  } catch (err) {
+    appendLog('getLatestPosition', { error: err.message });
+  }
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/.history/package_20250614100016.json b/.history/package_20250614100016.json
new file mode 100644
index 0000000..56cf604
--- /dev/null
+++ b/.history/package_20250614100016.json
@@ -0,0 +1,80 @@
+{
+  "name": "dumon-geolocation",
+  "version": "0.0.1",
+  "description": "Implement manager GNSS, Wiโ€‘Fi RTT, IMU, Kalman fusion, event emitter",
+  "main": "dist/plugin.cjs.js",
+  "module": "dist/esm/index.js",
+  "types": "dist/esm/index.d.ts",
+  "unpkg": "dist/plugin.js",
+  "files": [
+    "android/src/main/",
+    "android/build.gradle",
+    "dist/",
+    "ios/Sources",
+    "ios/Tests",
+    "Package.swift",
+    "DumonGeolocation.podspec"
+  ],
+  "author": "Donald Wengki Goni",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "git+https://git.jayacita.com/wengki/dumon-geolocation.git.git"
+  },
+  "bugs": {
+    "url": "https://git.jayacita.com/wengki/dumon-geolocation.git/issues"
+  },
+  "keywords": [
+    "capacitor",
+    "plugin",
+    "native"
+  ],
+  "scripts": {
+    "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
+    "verify:ios": "xcodebuild -scheme DumonGeolocation -destination generic/platform=iOS",
+    "verify:android": "cd android && ./gradlew clean build test && cd ..",
+    "verify:web": "npm run build",
+    "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
+    "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
+    "eslint": "eslint . --ext ts",
+    "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
+    "swiftlint": "node-swiftlint",
+    "docgen": "docgen --api DumonGeolocationPlugin --output-readme README.md --output-json dist/docs.json",
+    "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
+    "clean": "rimraf ./dist",
+    "watch": "tsc --watch",
+    "prepublishOnly": "npm run build"
+  },
+  "devDependencies": {
+    "@capacitor/android": "^7.0.0",
+    "@capacitor/core": "^7.0.0",
+    "@capacitor/docgen": "^0.3.0",
+    "@capacitor/ios": "^7.0.0",
+    "@ionic/eslint-config": "^0.4.0",
+    "@ionic/prettier-config": "^4.0.0",
+    "@ionic/swiftlint-config": "^2.0.0",
+    "eslint": "^8.57.0",
+    "prettier": "^3.4.2",
+    "prettier-plugin-java": "^2.6.6",
+    "rimraf": "^6.0.1",
+    "rollup": "^4.30.1",
+    "swiftlint": "^2.0.0",
+    "typescript": "~4.1.5"
+  },
+  "peerDependencies": {
+    "@capacitor/core": ">=7.0.0"
+  },
+  "prettier": "@ionic/prettier-config",
+  "swiftlint": "@ionic/swiftlint-config",
+  "eslintConfig": {
+    "extends": "@ionic/eslint-config/recommended"
+  },
+  "capacitor": {
+    "ios": {
+      "src": "ios"
+    },
+    "android": {
+      "src": "android"
+    }
+  }
+}
diff --git a/.history/package_20250614102416.json b/.history/package_20250614102416.json
new file mode 100644
index 0000000..9e3f39a
--- /dev/null
+++ b/.history/package_20250614102416.json
@@ -0,0 +1,80 @@
+{
+  "name": "dumon-geolocation",
+  "version": "0.0.1",
+  "description": "Implement manager GNSS, Wiโ€‘Fi RTT, IMU, Kalman fusion, event emitter",
+  "main": "dist/plugin.cjs.js",
+  "module": "dist/esm/index.js",
+  "types": "dist/esm/index.d.ts",
+  "unpkg": "dist/plugin.js",
+  "files": [
+    "android/src/main/",
+    "android/build.gradle",
+    "dist/",
+    "ios/Sources",
+    "ios/Tests",
+    "Package.swift",
+    "DumonGeolocation.podspec"
+  ],
+  "author": "Donald Wengki Goni",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "git+https://git.jayacita.com/wengki/dumon-geolocation.git.git"
+  },
+  "bugs": {
+    "url": "https://git.jayacita.com/wengki/dumon-geolocation.git/issues"
+  },
+  "keywords": [
+    "capacitor",
+    "plugin",
+    "native"
+  ],
+  "scripts": {
+    "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
+    "verify:ios": "xcodebuild -scheme DumonGeolocation -destination generic/platform=iOS",
+    "verify:android": "cd android && ./gradlew clean build test && cd ..",
+    "verify:web": "npm run build",
+    "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
+    "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
+    "eslint": "eslint . --ext ts",
+    "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
+    "swiftlint": "node-swiftlint",
+    "docgen": "docgen --api DumonGeolocationPlugin --output-readme README.md --output-json dist/docs.json",
+    "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
+    "clean": "rimraf ./dist",
+    "watch": "tsc --watch",
+    "prepublishOnly": "npm run build"
+  },
+  "devDependencies": {
+    "@capacitor/android": "^6.0.0",
+    "@capacitor/core": "^6.0.0",
+    "@capacitor/docgen": "^0.3.0",
+    "@capacitor/ios": "^6.0.0",
+    "@ionic/eslint-config": "^0.4.0",
+    "@ionic/prettier-config": "^4.0.0",
+    "@ionic/swiftlint-config": "^2.0.0",
+    "eslint": "^8.57.0",
+    "prettier": "^3.4.2",
+    "prettier-plugin-java": "^2.6.6",
+    "rimraf": "^6.0.1",
+    "rollup": "^4.30.1",
+    "swiftlint": "^2.0.0",
+    "typescript": "~4.1.5"
+  },
+  "peerDependencies": {
+    "@capacitor/core": ">=6.0.0"
+  },
+  "prettier": "@ionic/prettier-config",
+  "swiftlint": "@ionic/swiftlint-config",
+  "eslintConfig": {
+    "extends": "@ionic/eslint-config/recommended"
+  },
+  "capacitor": {
+    "ios": {
+      "src": "ios"
+    },
+    "android": {
+      "src": "android"
+    }
+  }
+}
diff --git a/.history/package_20250614102435.json b/.history/package_20250614102435.json
new file mode 100644
index 0000000..56cf604
--- /dev/null
+++ b/.history/package_20250614102435.json
@@ -0,0 +1,80 @@
+{
+  "name": "dumon-geolocation",
+  "version": "0.0.1",
+  "description": "Implement manager GNSS, Wiโ€‘Fi RTT, IMU, Kalman fusion, event emitter",
+  "main": "dist/plugin.cjs.js",
+  "module": "dist/esm/index.js",
+  "types": "dist/esm/index.d.ts",
+  "unpkg": "dist/plugin.js",
+  "files": [
+    "android/src/main/",
+    "android/build.gradle",
+    "dist/",
+    "ios/Sources",
+    "ios/Tests",
+    "Package.swift",
+    "DumonGeolocation.podspec"
+  ],
+  "author": "Donald Wengki Goni",
+  "license": "MIT",
+  "repository": {
+    "type": "git",
+    "url": "git+https://git.jayacita.com/wengki/dumon-geolocation.git.git"
+  },
+  "bugs": {
+    "url": "https://git.jayacita.com/wengki/dumon-geolocation.git/issues"
+  },
+  "keywords": [
+    "capacitor",
+    "plugin",
+    "native"
+  ],
+  "scripts": {
+    "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
+    "verify:ios": "xcodebuild -scheme DumonGeolocation -destination generic/platform=iOS",
+    "verify:android": "cd android && ./gradlew clean build test && cd ..",
+    "verify:web": "npm run build",
+    "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
+    "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
+    "eslint": "eslint . --ext ts",
+    "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
+    "swiftlint": "node-swiftlint",
+    "docgen": "docgen --api DumonGeolocationPlugin --output-readme README.md --output-json dist/docs.json",
+    "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
+    "clean": "rimraf ./dist",
+    "watch": "tsc --watch",
+    "prepublishOnly": "npm run build"
+  },
+  "devDependencies": {
+    "@capacitor/android": "^7.0.0",
+    "@capacitor/core": "^7.0.0",
+    "@capacitor/docgen": "^0.3.0",
+    "@capacitor/ios": "^7.0.0",
+    "@ionic/eslint-config": "^0.4.0",
+    "@ionic/prettier-config": "^4.0.0",
+    "@ionic/swiftlint-config": "^2.0.0",
+    "eslint": "^8.57.0",
+    "prettier": "^3.4.2",
+    "prettier-plugin-java": "^2.6.6",
+    "rimraf": "^6.0.1",
+    "rollup": "^4.30.1",
+    "swiftlint": "^2.0.0",
+    "typescript": "~4.1.5"
+  },
+  "peerDependencies": {
+    "@capacitor/core": ">=7.0.0"
+  },
+  "prettier": "@ionic/prettier-config",
+  "swiftlint": "@ionic/swiftlint-config",
+  "eslintConfig": {
+    "extends": "@ionic/eslint-config/recommended"
+  },
+  "capacitor": {
+    "ios": {
+      "src": "ios"
+    },
+    "android": {
+      "src": "android"
+    }
+  }
+}
diff --git a/.history/src/definitions_20250614100016.ts b/.history/src/definitions_20250614100016.ts
new file mode 100644
index 0000000..9ae8e14
--- /dev/null
+++ b/.history/src/definitions_20250614100016.ts
@@ -0,0 +1,3 @@
+export interface DumonGeolocationPlugin {
+  echo(options: { value: string }): Promise<{ value: string }>;
+}
diff --git a/.history/src/definitions_20250614103117.ts b/.history/src/definitions_20250614103117.ts
new file mode 100644
index 0000000..1f92b91
--- /dev/null
+++ b/.history/src/definitions_20250614103117.ts
@@ -0,0 +1,24 @@
+import { PluginListenerHandle } from '@capacitor/core';
+
+export interface PositioningData {
+  latitude: number;
+  longitude: number;
+  accuracy: number;
+  source: 'GNSS' | 'WIFI' | 'FUSED';
+  imuData: {
+    accelX: number;
+    accelY: number;
+    accelZ: number;
+    gyroX: number;
+    gyroY: number;
+    gyroZ: number;
+  };
+}
+
+export interface DumonGeolocationPlugin {
+  startPositioning(): Promise;
+  stopPositioning(): Promise;
+  getLatestPosition(): Promise;
+
+  addListener(eventName: 'onPositionUpdate', listenerFunc: (data: PositioningData) => void): PluginListenerHandle;
+}
diff --git a/.history/src/definitions_20250614103144.ts b/.history/src/definitions_20250614103144.ts
new file mode 100644
index 0000000..d721612
--- /dev/null
+++ b/.history/src/definitions_20250614103144.ts
@@ -0,0 +1,24 @@
+import type { PluginListenerHandle } from '@capacitor/core';
+
+export interface PositioningData {
+  latitude: number;
+  longitude: number;
+  accuracy: number;
+  source: 'GNSS' | 'WIFI' | 'FUSED';
+  imuData: {
+    accelX: number;
+    accelY: number;
+    accelZ: number;
+    gyroX: number;
+    gyroY: number;
+    gyroZ: number;
+  };
+}
+
+export interface DumonGeolocationPlugin {
+  startPositioning(): Promise;
+  stopPositioning(): Promise;
+  getLatestPosition(): Promise;
+
+  addListener(eventName: 'onPositionUpdate', listenerFunc: (data: PositioningData) => void): PluginListenerHandle;
+}
diff --git a/.history/src/definitions_20250614113628.ts b/.history/src/definitions_20250614113628.ts
new file mode 100644
index 0000000..d9fc674
--- /dev/null
+++ b/.history/src/definitions_20250614113628.ts
@@ -0,0 +1,53 @@
+import type { PluginListenerHandle } from '@capacitor/core';
+
+export interface SatelliteStatus {
+  satellitesInView: number;
+  usedInFix: number;
+  constellationCounts: { [key: string]: number };
+}
+
+export interface WifiAp {
+  ssid: string;
+  bssid: string;
+  rssi: number;
+  distance?: number;
+}
+
+export interface WifiScanResult {
+  apCount: number;
+  aps: WifiAp[];
+}
+
+export interface PositioningData {
+  source: 'GNSS' | 'WIFI' | 'FUSED';
+  timestamp: number;
+
+  gpsData: {
+    latitude: number;
+    longitude: number;
+    accuracy: number;
+    satellitesInView?: number;
+    usedInFix?: number;
+    constellationCounts?: { [key: string]: number };
+  };
+
+  wifiData: WifiScanResult;
+
+  imuData: {
+    accelX: number;
+    accelY: number;
+    accelZ: number;
+    gyroX: number;
+    gyroY: number;
+    gyroZ: number;
+  };
+}
+
+
+export interface DumonGeolocationPlugin {
+  startPositioning(): Promise;
+  stopPositioning(): Promise;
+  getLatestPosition(): Promise;
+
+  addListener(eventName: 'onPositionUpdate', listenerFunc: (data: PositioningData) => void): PluginListenerHandle;
+}
\ No newline at end of file
diff --git a/.history/src/definitions_20250614121305.ts b/.history/src/definitions_20250614121305.ts
new file mode 100644
index 0000000..475a629
--- /dev/null
+++ b/.history/src/definitions_20250614121305.ts
@@ -0,0 +1,56 @@
+import type { PluginListenerHandle } from '@capacitor/core';
+
+export interface SatelliteStatus {
+  satellitesInView: number;
+  usedInFix: number;
+  constellationCounts: { [key: string]: number };
+}
+
+export interface WifiAp {
+  ssid: string;
+  bssid: string;
+  rssi: number;
+  distance?: number;
+}
+
+export interface WifiScanResult {
+  apCount: number;
+  aps: WifiAp[];
+}
+
+export interface ImuData {
+  accelX: number;
+  accelY: number;
+  accelZ: number;
+  gyroX: number;
+  gyroY: number;
+  gyroZ: number;
+}
+
+export interface GpsData {
+  latitude: number;
+  longitude: number;
+  accuracy: number;
+  satellitesInView?: number;
+  usedInFix?: number;
+  constellationCounts?: { [key: string]: number };
+}
+
+export interface PositioningData {
+  source: 'GNSS' | 'WIFI' | 'FUSED';
+  timestamp: number;
+  gpsData: GpsData;
+  wifiData: WifiScanResult;
+  imuData: ImuData;
+}
+
+export interface DumonGeolocationPlugin {
+  startPositioning(): Promise;
+  stopPositioning(): Promise;
+  getLatestPosition(): Promise;
+
+  addListener(
+    eventName: 'onPositionUpdate',
+    listenerFunc: (data: PositioningData) => void
+  ): PluginListenerHandle;
+}
\ No newline at end of file
diff --git a/.history/src/definitions_20250614135647.ts b/.history/src/definitions_20250614135647.ts
new file mode 100644
index 0000000..1c0c36c
--- /dev/null
+++ b/.history/src/definitions_20250614135647.ts
@@ -0,0 +1,63 @@
+import type { PluginListenerHandle } from '@capacitor/core';
+
+export interface SatelliteStatus {
+  satellitesInView: number;
+  usedInFix: number;
+  constellationCounts: { [key: string]: number };
+}
+
+export interface WifiAp {
+  ssid: string;
+  bssid: string;
+  rssi: number;
+  distance?: number;
+}
+
+export interface WifiScanResult {
+  apCount: number;
+  aps: WifiAp[];
+}
+
+export interface ImuData {
+  accelX: number;
+  accelY: number;
+  accelZ: number;
+  gyroX: number;
+  gyroY: number;
+  gyroZ: number;
+  speed?: number;
+  acceleration?: number;
+  directionRad?: number;
+}
+
+export interface GpsData {
+  latitude: number;
+  longitude: number;
+  accuracy: number;
+  satellitesInView?: number;
+  usedInFix?: number;
+  constellationCounts?: { [key: string]: number };
+}
+
+export interface PositioningData {
+  source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';
+  timestamp: number;
+  latitude: number;
+  longitude: number;
+  accuracy: number;
+
+  gnssData?: SatelliteStatus;
+  wifiData?: WifiAp[];
+  imuData?: ImuData;
+}
+
+export interface DumonGeolocationPlugin {
+  startPositioning(): Promise;
+  stopPositioning(): Promise;
+  getLatestPosition(): Promise;
+
+  addListener(
+    eventName: 'onPositionUpdate',
+    listenerFunc: (data: PositioningData) => void
+  ): PluginListenerHandle;
+}
\ No newline at end of file
diff --git a/.history/src/index_20250614100016.ts b/.history/src/index_20250614100016.ts
new file mode 100644
index 0000000..4f8aba3
--- /dev/null
+++ b/.history/src/index_20250614100016.ts
@@ -0,0 +1,10 @@
+import { registerPlugin } from '@capacitor/core';
+
+import type { DumonGeolocationPlugin } from './definitions';
+
+const DumonGeolocation = registerPlugin('DumonGeolocation', {
+  web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),
+});
+
+export * from './definitions';
+export { DumonGeolocation };
diff --git a/.history/src/index_20250614103213.ts b/.history/src/index_20250614103213.ts
new file mode 100644
index 0000000..4367fe5
--- /dev/null
+++ b/.history/src/index_20250614103213.ts
@@ -0,0 +1,10 @@
+import { registerPlugin } from '@capacitor/core';
+
+import type { DumonGeolocationPlugin } from './definitions';
+
+const DumonGeolocation = registerPlugin('DumonGeolocation', {
+  web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),
+});
+
+export * from './definitions';
+export { DumonGeolocation };
\ No newline at end of file
diff --git a/.history/src/index_20250614121335.ts b/.history/src/index_20250614121335.ts
new file mode 100644
index 0000000..4367fe5
--- /dev/null
+++ b/.history/src/index_20250614121335.ts
@@ -0,0 +1,10 @@
+import { registerPlugin } from '@capacitor/core';
+
+import type { DumonGeolocationPlugin } from './definitions';
+
+const DumonGeolocation = registerPlugin('DumonGeolocation', {
+  web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),
+});
+
+export * from './definitions';
+export { DumonGeolocation };
\ No newline at end of file
diff --git a/.history/src/js/example_20250614103439.js b/.history/src/js/example_20250614103439.js
new file mode 100644
index 0000000..61bc127
--- /dev/null
+++ b/.history/src/js/example_20250614103439.js
@@ -0,0 +1,24 @@
+import { DumonGeolocation } from '@dumon/capacitor-geolocation';
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    console.log('[onPositionUpdate]', data);
+  });
+
+  await DumonGeolocation.startPositioning();
+}
+
+async function stopGeolocation() {
+  await DumonGeolocation.stopPositioning();
+}
+
+async function getLatestPosition() {
+  const data = await DumonGeolocation.getLatestPosition();
+  console.log('[getLatestPosition]', data);
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/.history/src/js/example_20250614103448.js b/.history/src/js/example_20250614103448.js
new file mode 100644
index 0000000..61bc127
--- /dev/null
+++ b/.history/src/js/example_20250614103448.js
@@ -0,0 +1,24 @@
+import { DumonGeolocation } from '@dumon/capacitor-geolocation';
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    console.log('[onPositionUpdate]', data);
+  });
+
+  await DumonGeolocation.startPositioning();
+}
+
+async function stopGeolocation() {
+  await DumonGeolocation.stopPositioning();
+}
+
+async function getLatestPosition() {
+  const data = await DumonGeolocation.getLatestPosition();
+  console.log('[getLatestPosition]', data);
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/.history/src/js/example_20250614105124.js b/.history/src/js/example_20250614105124.js
new file mode 100644
index 0000000..f49f527
--- /dev/null
+++ b/.history/src/js/example_20250614105124.js
@@ -0,0 +1,24 @@
+import { DumonGeolocation } from 'dumon-geolocation';
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    console.log('[onPositionUpdate]', data);
+  });
+
+  await DumonGeolocation.startPositioning();
+}
+
+async function stopGeolocation() {
+  await DumonGeolocation.stopPositioning();
+}
+
+async function getLatestPosition() {
+  const data = await DumonGeolocation.getLatestPosition();
+  console.log('[getLatestPosition]', data);
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/.history/src/web_20250614100016.ts b/.history/src/web_20250614100016.ts
new file mode 100644
index 0000000..f0e5f85
--- /dev/null
+++ b/.history/src/web_20250614100016.ts
@@ -0,0 +1,10 @@
+import { WebPlugin } from '@capacitor/core';
+
+import type { DumonGeolocationPlugin } from './definitions';
+
+export class DumonGeolocationWeb extends WebPlugin implements DumonGeolocationPlugin {
+  async echo(options: { value: string }): Promise<{ value: string }> {
+    console.log('ECHO', options);
+    return options;
+  }
+}
diff --git a/.history/src/web_20250614103423.ts b/.history/src/web_20250614103423.ts
new file mode 100644
index 0000000..d380e0f
--- /dev/null
+++ b/.history/src/web_20250614103423.ts
@@ -0,0 +1,33 @@
+import { WebPlugin } from '@capacitor/core';
+
+import type { PositioningData, DumonGeolocationPlugin } from './definitions';
+
+export class DumonGeolocationWeb extends WebPlugin implements DumonGeolocationPlugin {
+
+  async startPositioning(): Promise {
+    console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
+  }
+
+  async stopPositioning(): Promise {
+    console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
+  }
+
+  async getLatestPosition(): Promise {
+    console.log('DumonGeolocationWeb: getLatestPosition() called (no-op)');
+    return {
+      latitude: 0,
+      longitude: 0,
+      accuracy: 999,
+      source: 'GNSS',
+      imuData: {
+        accelX: 0,
+        accelY: 0,
+        accelZ: 0,
+        gyroX: 0,
+        gyroY: 0,
+        gyroZ: 0,
+      },
+    };
+  }
+
+}
\ No newline at end of file
diff --git a/.history/src/web_20250614103640.ts b/.history/src/web_20250614103640.ts
new file mode 100644
index 0000000..7aaa92f
--- /dev/null
+++ b/.history/src/web_20250614103640.ts
@@ -0,0 +1,33 @@
+import { WebPlugin } from '@capacitor/core';
+
+import type { PositioningData } from './definitions';
+
+export class DumonGeolocationWeb extends WebPlugin {
+
+  async startPositioning(): Promise {
+    console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
+  }
+
+  async stopPositioning(): Promise {
+    console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
+  }
+
+  async getLatestPosition(): Promise {
+    console.log('DumonGeolocationWeb: getLatestPosition() called (no-op)');
+    return {
+      latitude: 0,
+      longitude: 0,
+      accuracy: 999,
+      source: 'GNSS',
+      imuData: {
+        accelX: 0,
+        accelY: 0,
+        accelZ: 0,
+        gyroX: 0,
+        gyroY: 0,
+        gyroZ: 0,
+      },
+    };
+  }
+
+}
\ No newline at end of file
diff --git a/.history/src/web_20250614114331.ts b/.history/src/web_20250614114331.ts
new file mode 100644
index 0000000..fe004b2
--- /dev/null
+++ b/.history/src/web_20250614114331.ts
@@ -0,0 +1,39 @@
+import { WebPlugin } from '@capacitor/core';
+import type { PositioningData } from './definitions';
+
+export class DumonGeolocationWeb extends WebPlugin {
+  async startPositioning(): Promise {
+    console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
+  }
+
+  async stopPositioning(): Promise {
+    console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
+  }
+
+  async getLatestPosition(): Promise {
+    return {
+      source: 'GNSS',
+      timestamp: Date.now(),
+      gpsData: {
+        latitude: 0,
+        longitude: 0,
+        accuracy: 999,
+        satellitesInView: 0,
+        usedInFix: 0,
+        constellationCounts: {}
+      },
+      wifiData: {
+        apCount: 0,
+        aps: []
+      },
+      imuData: {
+        accelX: 0,
+        accelY: 0,
+        accelZ: 0,
+        gyroX: 0,
+        gyroY: 0,
+        gyroZ: 0
+      }
+    };
+  }
+}
\ No newline at end of file
diff --git a/.history/src/web_20250614121358.ts b/.history/src/web_20250614121358.ts
new file mode 100644
index 0000000..4674356
--- /dev/null
+++ b/.history/src/web_20250614121358.ts
@@ -0,0 +1,41 @@
+import { WebPlugin } from '@capacitor/core';
+import type { DumonGeolocationPlugin, PositioningData } from './definitions';
+
+export class DumonGeolocationWeb extends WebPlugin implements DumonGeolocationPlugin {
+
+  async startPositioning(): Promise {
+    console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
+  }
+
+  async stopPositioning(): Promise {
+    console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
+  }
+
+  async getLatestPosition(): Promise {
+    console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');
+    return {
+      source: 'GNSS',
+      timestamp: Date.now(),
+      gpsData: {
+        latitude: 0,
+        longitude: 0,
+        accuracy: 999,
+        satellitesInView: 0,
+        usedInFix: 0,
+        constellationCounts: {},
+      },
+      wifiData: {
+        apCount: 0,
+        aps: [],
+      },
+      imuData: {
+        accelX: 0,
+        accelY: 0,
+        accelZ: 0,
+        gyroX: 0,
+        gyroY: 0,
+        gyroZ: 0,
+      }
+    };
+  }
+}
\ No newline at end of file
diff --git a/.history/src/web_20250614121612.ts b/.history/src/web_20250614121612.ts
new file mode 100644
index 0000000..f620fa0
--- /dev/null
+++ b/.history/src/web_20250614121612.ts
@@ -0,0 +1,41 @@
+import { WebPlugin } from '@capacitor/core';
+import type { DumonGeolocationPlugin, PositioningData } from './definitions';
+
+export class DumonGeolocationWeb extends WebPlugin {
+
+  async startPositioning(): Promise {
+    console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
+  }
+
+  async stopPositioning(): Promise {
+    console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
+  }
+
+  async getLatestPosition(): Promise {
+    console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');
+    return {
+      source: 'GNSS',
+      timestamp: Date.now(),
+      gpsData: {
+        latitude: 0,
+        longitude: 0,
+        accuracy: 999,
+        satellitesInView: 0,
+        usedInFix: 0,
+        constellationCounts: {},
+      },
+      wifiData: {
+        apCount: 0,
+        aps: [],
+      },
+      imuData: {
+        accelX: 0,
+        accelY: 0,
+        accelZ: 0,
+        gyroX: 0,
+        gyroY: 0,
+        gyroZ: 0,
+      }
+    };
+  }
+}
\ No newline at end of file
diff --git a/.history/src/web_20250614121624.ts b/.history/src/web_20250614121624.ts
new file mode 100644
index 0000000..fcb0236
--- /dev/null
+++ b/.history/src/web_20250614121624.ts
@@ -0,0 +1,41 @@
+import { WebPlugin } from '@capacitor/core';
+import type { PositioningData } from './definitions';
+
+export class DumonGeolocationWeb extends WebPlugin {
+
+  async startPositioning(): Promise {
+    console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
+  }
+
+  async stopPositioning(): Promise {
+    console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
+  }
+
+  async getLatestPosition(): Promise {
+    console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');
+    return {
+      source: 'GNSS',
+      timestamp: Date.now(),
+      gpsData: {
+        latitude: 0,
+        longitude: 0,
+        accuracy: 999,
+        satellitesInView: 0,
+        usedInFix: 0,
+        constellationCounts: {},
+      },
+      wifiData: {
+        apCount: 0,
+        aps: [],
+      },
+      imuData: {
+        accelX: 0,
+        accelY: 0,
+        accelZ: 0,
+        gyroX: 0,
+        gyroY: 0,
+        gyroZ: 0,
+      }
+    };
+  }
+}
\ No newline at end of file
diff --git a/.history/src/web_20250614135757.ts b/.history/src/web_20250614135757.ts
new file mode 100644
index 0000000..0162dd4
--- /dev/null
+++ b/.history/src/web_20250614135757.ts
@@ -0,0 +1,40 @@
+import { WebPlugin } from '@capacitor/core';
+import type { PositioningData, DumonGeolocationPlugin } from './definitions';
+
+export class DumonGeolocationWeb extends WebPlugin implements DumonGeolocationPlugin {
+  async startPositioning(): Promise {
+    console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
+  }
+
+  async stopPositioning(): Promise {
+    console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
+  }
+
+  async getLatestPosition(): Promise {
+    console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');
+    return {
+      source: 'GNSS',
+      timestamp: Date.now(),
+      latitude: 0,
+      longitude: 0,
+      accuracy: 999,
+      gnssData: {
+        satellitesInView: 0,
+        usedInFix: 0,
+        constellationCounts: {}
+      },
+      wifiData: [],
+      imuData: {
+        accelX: 0,
+        accelY: 0,
+        accelZ: 0,
+        gyroX: 0,
+        gyroY: 0,
+        gyroZ: 0,
+        speed: 0,
+        acceleration: 0,
+        directionRad: 0
+      }
+    };
+  }
+}
\ No newline at end of file
diff --git a/.history/src/web_20250614135839.ts b/.history/src/web_20250614135839.ts
new file mode 100644
index 0000000..a5c2df4
--- /dev/null
+++ b/.history/src/web_20250614135839.ts
@@ -0,0 +1,40 @@
+import { WebPlugin } from '@capacitor/core';
+import type { PositioningData } from './definitions';
+
+export class DumonGeolocationWeb extends WebPlugin {
+  async startPositioning(): Promise {
+    console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
+  }
+
+  async stopPositioning(): Promise {
+    console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
+  }
+
+  async getLatestPosition(): Promise {
+    console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');
+    return {
+      source: 'GNSS',
+      timestamp: Date.now(),
+      latitude: 0,
+      longitude: 0,
+      accuracy: 999,
+      gnssData: {
+        satellitesInView: 0,
+        usedInFix: 0,
+        constellationCounts: {}
+      },
+      wifiData: [],
+      imuData: {
+        accelX: 0,
+        accelY: 0,
+        accelZ: 0,
+        gyroX: 0,
+        gyroY: 0,
+        gyroZ: 0,
+        speed: 0,
+        acceleration: 0,
+        directionRad: 0
+      }
+    };
+  }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 3fe13e9..e29475c 100644
--- a/README.md
+++ b/README.md
@@ -1,37 +1,133 @@
 # dumon-geolocation
 
-Implement manager GNSS, Wiโ€‘Fi RTT, IMU, Kalman fusion, event emitter
+Plugin Capacitor Android untuk positioning real-time berbasis GNSS multi-konstelasi, Wi-Fi RTT/RSSI, dan IMU (Accelerometer + Gyroscope), dilengkapi dengan sensor fusion (Kalman Filter) dan deteksi lokasi palsu.
 
-## Install
+## ๐Ÿ“ฆ Install
 
 ```bash
 npm install dumon-geolocation
 npx cap sync
 ```
 
-## API
+## ๐Ÿš€ API
 
-
+### ๐Ÿ“ก startPositioning()
 
-* [`echo(...)`](#echo)
-
-
-
-
-
-
-### echo(...)
-
-```typescript
-echo(options: { value: string; }) => Promise<{ value: string; }>
+```ts
+startPositioning() => Promise
 ```
 
-| Param         | Type                            |
-| ------------- | ------------------------------- |
-| **`options`** | { value: string; } |
+Memulai pengambilan data posisi secara real-time dari GNSS, Wi-Fi, dan IMU.
 
-**Returns:** Promise<{ value: string; }>
+---
 
---------------------
+### ๐Ÿ›‘ stopPositioning()
 
-
+```ts
+stopPositioning() => Promise
+```
+
+Menghentikan semua sensor dan positioning.
+
+---
+
+### ๐Ÿ“ getLatestPosition()
+
+```ts
+getLatestPosition() => Promise
+```
+
+Mengembalikan data posisi terkini yang telah difusi.
+
+---
+
+### ๐Ÿ”„ addListener('onPositionUpdate', ...)
+
+```ts
+addListener(eventName: 'onPositionUpdate', listenerFunc: (data: PositioningData) => void): PluginListenerHandle
+```
+
+Listener untuk update posisi secara berkala (real-time).
+
+---
+
+## ๐Ÿงพ Interfaces
+
+### PositioningData
+
+```ts
+interface PositioningData {
+  source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';
+  latitude: number;
+  longitude: number;
+  accuracy: number;
+  speed: number;
+  acceleration: number;
+  directionRad: number;
+  timestamp: number;
+  isMocked: boolean;
+
+  // Optional raw sensor data (available internally)
+  // imuData?: ImuData;
+  // gnssData?: SatelliteStatus;
+  // wifiData?: WifiAp[];
+}
+```
+
+### ImuData
+
+```ts
+interface ImuData {
+  accelX: number;
+  accelY: number;
+  accelZ: number;
+  gyroX: number;
+  gyroY: number;
+  gyroZ: number;
+  speed: number;
+  acceleration: number;
+  directionRad: number;
+}
+```
+
+### SatelliteStatus
+
+```ts
+interface SatelliteStatus {
+  satellitesInView: number;
+  usedInFix: number;
+  constellationCounts: { [key: string]: number };
+}
+```
+
+### WifiAp
+
+```ts
+interface WifiAp {
+  ssid: string;
+  bssid: string;
+  rssi: number;
+  distance?: number;
+}
+```
+
+### PluginListenerHandle
+
+```ts
+interface PluginListenerHandle {
+  remove: () => Promise;
+}
+```
+
+---
+
+## โ„น๏ธ Catatan
+- Plugin hanya mendukung platform Android saat ini.
+- Ideal digunakan bersama dengan plugin `Geolocation` bawaan Capacitor untuk fallback atau perbandingan.
+- Sensor fusion berbasis Kalman Filter (versi sederhana).
+- `directionRad` merujuk arah dalam radian relatif terhadap utara (azimuth).
+- Output `isMocked` berguna untuk deteksi lokasi palsu.
+
+---
+
+Lisensi: MIT โ€“ Dibuat oleh Tim Dumon
\ No newline at end of file
diff --git a/android/build.gradle b/android/build.gradle
index b2c4c08..9940916 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -6,22 +6,27 @@ ext {
 }
 
 buildscript {
+    ext {
+        kotlin_version = '1.9.24'
+    }
     repositories {
         google()
         mavenCentral()
     }
     dependencies {
         classpath 'com.android.tools.build:gradle:8.7.2'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
     }
 }
 
 apply plugin: 'com.android.library'
+apply plugin: 'org.jetbrains.kotlin.android'
 
 android {
     namespace "com.dumon.plugin.geolocation"
     compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
     defaultConfig {
-        minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
+        minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
         targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
         versionCode 1
         versionName "1.0"
@@ -37,8 +42,11 @@ android {
         abortOnError false
     }
     compileOptions {
-        sourceCompatibility JavaVersion.VERSION_21
-        targetCompatibility JavaVersion.VERSION_21
+        sourceCompatibility JavaVersion.VERSION_17
+        targetCompatibility JavaVersion.VERSION_17
+    }
+    kotlinOptions {
+        jvmTarget = '17'
     }
 }
 
@@ -52,6 +60,7 @@ dependencies {
     implementation fileTree(dir: 'libs', include: ['*.jar'])
     implementation project(':capacitor-android')
     implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
+    implementation 'androidx.core:core-ktx:1.16.0'
     testImplementation "junit:junit:$junitVersion"
     androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
     androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index a2f47b6..ef0d267 100644
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,2 +1,10 @@
 
+    
+    
+    
+    
+    
+    
+    
+    
 
diff --git a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt b/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt
new file mode 100644
index 0000000..dc6d3ab
--- /dev/null
+++ b/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt
@@ -0,0 +1,177 @@
+package com.dumon.plugin.geolocation
+
+import android.Manifest
+import com.getcapacitor.*
+import com.getcapacitor.annotation.CapacitorPlugin
+import com.getcapacitor.annotation.Permission
+import com.dumon.plugin.geolocation.gps.GpsStatusManager
+import com.dumon.plugin.geolocation.gps.SatelliteStatus
+import com.dumon.plugin.geolocation.imu.ImuData
+import com.dumon.plugin.geolocation.imu.ImuSensorManager
+import com.dumon.plugin.geolocation.wifi.WifiPositioningManager
+import com.dumon.plugin.geolocation.wifi.WifiScanResult
+import com.dumon.plugin.geolocation.fusion.SensorFusionManager
+import org.json.JSONArray
+import org.json.JSONObject
+
+@CapacitorPlugin(
+    name = "DumonGeolocation",
+    permissions = [
+        Permission(strings = [
+            Manifest.permission.ACCESS_FINE_LOCATION,
+            Manifest.permission.ACCESS_COARSE_LOCATION,
+            Manifest.permission.ACCESS_WIFI_STATE,
+            Manifest.permission.CHANGE_WIFI_STATE,
+            Manifest.permission.NEARBY_WIFI_DEVICES
+        ])
+    ]
+)
+class DumonGeolocation : Plugin() {
+
+    private var gpsManager: GpsStatusManager? = null
+    private var imuManager: ImuSensorManager? = null
+    private var wifiManager: WifiPositioningManager? = null
+    private var fusionManager: SensorFusionManager? = null
+
+    private var latestLatitude = 0.0
+    private var latestLongitude = 0.0
+    private var latestAccuracy = 999.0
+    private var latestSource = "GNSS"
+    private var latestTimestamp: Long = 0L
+
+    private var latestImu: ImuData? = null
+    private var satelliteStatus: SatelliteStatus? = null
+    private var wifiScanResult: WifiScanResult? = null
+
+    private var isMockedLocation = false
+    private var lastEmitTimestamp: Long = 0L
+    private val emitIntervalMs: Long = 500L
+
+    override fun load() {
+        gpsManager = GpsStatusManager(
+            context,
+            onSatelliteStatusUpdate = { satelliteStatus = it },
+            onLocationUpdate = { location, isMocked ->
+                latestLatitude = location.latitude
+                latestLongitude = location.longitude
+                latestAccuracy = location.accuracy.toDouble()
+                latestSource = if (isMocked) "MOCK" else "GNSS"
+                isMockedLocation = isMocked
+                latestTimestamp = location.time
+                fusionManager?.updateGpsPosition(latestLatitude, latestLongitude)
+                emitPositionUpdate()
+            }
+        )
+
+        imuManager = ImuSensorManager(
+            context,
+            onImuUpdate = {
+                latestImu = it
+                emitPositionUpdate()
+            }
+        )
+
+        wifiManager = WifiPositioningManager(
+            context,
+            onWifiPositioningUpdate = {
+                wifiScanResult = it
+                emitPositionUpdate()
+            }
+        )
+
+        fusionManager = SensorFusionManager { lat, lon ->
+            latestLatitude = lat
+            latestLongitude = lon
+            latestAccuracy = 3.0
+            latestSource = "FUSED"
+            latestTimestamp = System.currentTimeMillis()
+            emitPositionUpdate()
+        }
+    }
+
+    @PluginMethod
+    fun startPositioning(call: PluginCall) {
+        gpsManager?.start()
+        imuManager?.start()
+        wifiManager?.startPeriodicScan(3000L)
+        call.resolve()
+    }
+
+    @PluginMethod
+    fun stopPositioning(call: PluginCall) {
+        gpsManager?.stop()
+        imuManager?.stop()
+        wifiManager?.stopPeriodicScan()
+        call.resolve()
+    }
+
+    @PluginMethod
+    fun getLatestPosition(call: PluginCall) {
+        call.resolve(buildPositionData())
+    }
+
+    private fun emitPositionUpdate() {
+        val now = System.currentTimeMillis()
+        if (now - lastEmitTimestamp < emitIntervalMs) return
+        lastEmitTimestamp = now
+        notifyListeners("onPositionUpdate", buildPositionData())
+    }
+
+    private fun buildPositionData(): JSObject {
+        val obj = JSObject()
+        obj.put("source", latestSource)
+        obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis())
+        obj.put("latitude", latestLatitude)
+        obj.put("longitude", latestLongitude)
+        obj.put("accuracy", latestAccuracy)
+        obj.put("isMocked", isMockedLocation)
+
+        latestImu?.let {
+            obj.put("speed", it.speed)
+            obj.put("acceleration", it.acceleration)
+            obj.put("directionRad", it.directionRad)
+        }
+
+        // === Full Detail (commented out for future use) ===
+        /*
+        satelliteStatus?.let {
+            val gnss = JSObject()
+            gnss.put("satellitesInView", it.satellitesInView)
+            gnss.put("usedInFix", it.usedInFix)
+            val constellations = JSObject()
+            it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) }
+            gnss.put("constellationCounts", constellations)
+            obj.put("gnssData", gnss)
+        }
+
+        latestImu?.let {
+            val imu = JSObject()
+            imu.put("accelX", it.accelX)
+            imu.put("accelY", it.accelY)
+            imu.put("accelZ", it.accelZ)
+            imu.put("gyroX", it.gyroX)
+            imu.put("gyroY", it.gyroY)
+            imu.put("gyroZ", it.gyroZ)
+            imu.put("speed", it.speed)
+            imu.put("acceleration", it.acceleration)
+            imu.put("directionRad", it.directionRad)
+            obj.put("imuData", imu)
+        }
+
+        wifiScanResult?.let {
+            val wifi = JSArray()
+            it.aps.forEach { ap ->
+                val a = JSObject()
+                a.put("ssid", ap.ssid)
+                a.put("bssid", ap.bssid)
+                a.put("rssi", ap.rssi)
+                ap.distance?.let { d -> a.put("distance", d) }
+                wifi.put(a)
+            }
+            obj.put("wifiData", wifi)
+        }
+        */
+
+        return obj
+    }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocationPlugin.java b/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocationPlugin.java
deleted file mode 100644
index c861586..0000000
--- a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocationPlugin.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.dumon.plugin.geolocation;
-
-import com.getcapacitor.JSObject;
-import com.getcapacitor.Plugin;
-import com.getcapacitor.PluginCall;
-import com.getcapacitor.PluginMethod;
-import com.getcapacitor.annotation.CapacitorPlugin;
-
-@CapacitorPlugin(name = "DumonGeolocation")
-public class DumonGeolocationPlugin extends Plugin {
-
-    private DumonGeolocation implementation = new DumonGeolocation();
-
-    @PluginMethod
-    public void echo(PluginCall call) {
-        String value = call.getString("value");
-
-        JSObject ret = new JSObject();
-        ret.put("value", implementation.echo(value));
-        call.resolve(ret);
-    }
-}
diff --git a/android/src/main/java/com/dumon/plugin/geolocation/fusion/SensorFusionManager.kt b/android/src/main/java/com/dumon/plugin/geolocation/fusion/SensorFusionManager.kt
new file mode 100644
index 0000000..32f4098
--- /dev/null
+++ b/android/src/main/java/com/dumon/plugin/geolocation/fusion/SensorFusionManager.kt
@@ -0,0 +1,61 @@
+package com.dumon.plugin.geolocation.fusion
+
+import android.util.Log
+
+class SensorFusionManager(
+    private val onFusedPositionUpdate: (Double, Double) -> Unit = { _, _ -> }
+) {
+
+    // Kalman filter state
+    private var latEstimate = 0.0
+    private var lonEstimate = 0.0
+    private var latErrorEstimate = 1.0
+    private var lonErrorEstimate = 1.0
+
+    private val processNoiseBase = 0.01
+    private val measurementNoise = 3.0 // typical GPS error in meters
+
+    private var isFirstUpdate = true
+    private var lastUpdateTimestamp: Long = 0L
+
+    fun updateGpsPosition(lat: Double, lon: Double) {
+        val currentTimestamp = System.currentTimeMillis()
+        val dtSeconds = if (lastUpdateTimestamp > 0) {
+            (currentTimestamp - lastUpdateTimestamp) / 1000.0
+        } else {
+            1.0
+        }
+        lastUpdateTimestamp = currentTimestamp
+
+        val processNoise = processNoiseBase * dtSeconds
+
+        if (isFirstUpdate) {
+            latEstimate = lat
+            lonEstimate = lon
+            isFirstUpdate = false
+        } else {
+            // Kalman update for latitude
+            val latKalmanGain = latErrorEstimate / (latErrorEstimate + measurementNoise)
+            latEstimate += latKalmanGain * (lat - latEstimate)
+            latErrorEstimate = (1 - latKalmanGain) * latErrorEstimate + processNoise
+
+            // Kalman update for longitude
+            val lonKalmanGain = lonErrorEstimate / (lonErrorEstimate + measurementNoise)
+            lonEstimate += lonKalmanGain * (lon - lonEstimate)
+            lonErrorEstimate = (1 - lonKalmanGain) * lonErrorEstimate + processNoise
+        }
+
+        Log.d("SENSOR_FUSION", "Fused Lat: $latEstimate, Lon: $lonEstimate")
+
+        onFusedPositionUpdate(latEstimate, lonEstimate)
+    }
+
+    fun reset() {
+        latEstimate = 0.0
+        lonEstimate = 0.0
+        latErrorEstimate = 1.0
+        lonErrorEstimate = 1.0
+        isFirstUpdate = true
+        lastUpdateTimestamp = 0L
+    }
+}
\ No newline at end of file
diff --git a/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager.kt b/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager.kt
new file mode 100644
index 0000000..4240646
--- /dev/null
+++ b/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager.kt
@@ -0,0 +1,129 @@
+package com.dumon.plugin.geolocation.gps
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.pm.PackageManager
+import android.location.GnssStatus
+import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import android.os.Build
+import android.os.Bundle
+import android.text.format.DateFormat
+import android.util.Log
+import androidx.core.app.ActivityCompat
+
+class GpsStatusManager(
+    private val context: Context,
+    private val onSatelliteStatusUpdate: (SatelliteStatus) -> Unit = {},
+    private val onLocationUpdate: (Location, Boolean) -> Unit = { _, _ -> }
+) {
+
+    private val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
+
+    private val gnssStatusCallback = object : GnssStatus.Callback() {
+        override fun onSatelliteStatusChanged(status: GnssStatus) {
+            val satelliteCount = status.satelliteCount
+            var usedInFixCount = 0
+            val constellationCounts = mutableMapOf()
+
+            for (i in 0 until satelliteCount) {
+                if (status.usedInFix(i)) usedInFixCount++
+                val constellationType = status.getConstellationType(i)
+                constellationCounts[constellationType] = constellationCounts.getOrDefault(constellationType, 0) + 1
+            }
+
+            val statusObj = SatelliteStatus(
+                satellitesInView = satelliteCount,
+                usedInFix = usedInFixCount,
+                constellationCounts = constellationCounts.mapKeys { getConstellationName(it.key) }
+            )
+
+            onSatelliteStatusUpdate(statusObj)
+        }
+    }
+
+    private val locationListener = object : LocationListener {
+        override fun onLocationChanged(location: Location) {
+            val providerTag = when (location.provider) {
+                LocationManager.GPS_PROVIDER -> "[GPS]"
+                LocationManager.NETWORK_PROVIDER -> "[NET]"
+                else -> "[OTHER]"
+            }
+
+            val timestamp = DateFormat.format("HH:mm:ss", System.currentTimeMillis())
+            val isMocked = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                location.isMock // API 31+
+            } else {
+                location.isFromMockProvider // Fallback
+            }
+
+            val info = "$providerTag Lat: ${location.latitude}, Lon: ${location.longitude}, Acc: ${location.accuracy} m @ $timestamp | Mock=$isMocked"
+            Log.d("GPS_LOCATION", info)
+
+            onLocationUpdate(location, isMocked)
+        }
+
+        @Deprecated("Deprecated in Java")
+        override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
+
+        override fun onProviderEnabled(provider: String) {}
+        override fun onProviderDisabled(provider: String) {}
+    }
+
+    @SuppressLint("MissingPermission")
+    fun start() {
+        try {
+            if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
+                ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
+                Log.e("GPS_STATUS", "Missing location permissions")
+                return
+            }
+
+            locationManager.registerGnssStatusCallback(gnssStatusCallback, null)
+
+            locationManager.requestLocationUpdates(
+                LocationManager.GPS_PROVIDER,
+                1000L,
+                0f,
+                locationListener
+            )
+
+            locationManager.requestLocationUpdates(
+                LocationManager.NETWORK_PROVIDER,
+                3000L,
+                10f,
+                locationListener
+            )
+
+            Log.d("GPS_STATUS", "GPS + Network location tracking started")
+        } catch (e: SecurityException) {
+            Log.e("GPS_STATUS", "SecurityException", e)
+        }
+    }
+
+    fun stop() {
+        locationManager.unregisterGnssStatusCallback(gnssStatusCallback)
+        locationManager.removeUpdates(locationListener)
+        Log.d("GPS_STATUS", "GPS tracking stopped")
+    }
+
+    private fun getConstellationName(type: Int): String {
+        return when (type) {
+            GnssStatus.CONSTELLATION_GPS -> "GPS"
+            GnssStatus.CONSTELLATION_SBAS -> "SBAS"
+            GnssStatus.CONSTELLATION_GLONASS -> "GLONASS"
+            GnssStatus.CONSTELLATION_QZSS -> "QZSS"
+            GnssStatus.CONSTELLATION_BEIDOU -> "BEIDOU"
+            GnssStatus.CONSTELLATION_GALILEO -> "GALILEO"
+            GnssStatus.CONSTELLATION_IRNSS -> "IRNSS"
+            else -> "Unknown ($type)"
+        }
+    }
+}
+
+data class SatelliteStatus(
+    val satellitesInView: Int,
+    val usedInFix: Int,
+    val constellationCounts: Map
+)
\ No newline at end of file
diff --git a/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager.kt b/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager.kt
new file mode 100644
index 0000000..61faf90
--- /dev/null
+++ b/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager.kt
@@ -0,0 +1,144 @@
+package com.dumon.plugin.geolocation.imu
+
+import android.content.Context
+import android.hardware.*
+import android.util.Log
+import kotlin.math.*
+
+class ImuSensorManager(
+    private val context: Context,
+    private val onImuUpdate: (ImuData) -> Unit
+) : SensorEventListener {
+
+    private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
+    private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_LINEAR_ACCELERATION)
+    private val gyroscope = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
+    private val rotationVector = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)
+
+    private var lastAccel = FloatArray(3) { 0f }
+    private var lastGyro = FloatArray(3) { 0f }
+    private var velocity = FloatArray(3) { 0f }
+    private var lastAccelTimestamp: Long = 0L
+
+    private val accelLowPass = FloatArray(3) { 0f }
+    private val alpha = 0.85f
+    private val accelerationThreshold = 0.12f
+    private val idleTimeoutNs = 2_000_000_000L // 2 seconds
+    private var lastActiveTimestamp: Long = 0L
+
+    private var latestDirectionRad = 0f
+    private var latestSpeed = 0f
+
+    fun start() {
+        accelerometer?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) }
+        gyroscope?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) }
+        rotationVector?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) }
+        Log.d("IMU_SENSOR", "IMU sensor tracking started")
+    }
+
+    fun stop() {
+        sensorManager.unregisterListener(this)
+        Log.d("IMU_SENSOR", "IMU sensor tracking stopped")
+    }
+
+    override fun onSensorChanged(event: SensorEvent?) {
+        event?.let {
+            when (it.sensor.type) {
+                Sensor.TYPE_LINEAR_ACCELERATION -> handleAccelerometer(it)
+                Sensor.TYPE_GYROSCOPE -> handleGyroscope(it)
+                Sensor.TYPE_ROTATION_VECTOR -> handleRotationVector(it)
+            }
+        }
+    }
+
+    private fun handleAccelerometer(event: SensorEvent) {
+        val currentTime = event.timestamp
+
+        for (i in 0..2) {
+            accelLowPass[i] = alpha * accelLowPass[i] + (1 - alpha) * event.values[i]
+        }
+
+        val accMag = sqrt(accelLowPass.map { it * it }.sum())
+
+        if (accMag > accelerationThreshold) {
+            if (lastAccelTimestamp != 0L) {
+                val dt = (currentTime - lastAccelTimestamp) / 1_000_000_000f
+                for (i in 0..2) {
+                    velocity[i] += accelLowPass[i] * dt
+                }
+            }
+            lastActiveTimestamp = currentTime
+        } else {
+            if (currentTime - lastActiveTimestamp > idleTimeoutNs) {
+                for (i in 0..2) velocity[i] = 0f
+            } else {
+                for (i in 0..2) velocity[i] *= 0.96f
+            }
+        }
+
+        lastAccelTimestamp = currentTime
+        lastAccel = accelLowPass.copyOf()
+
+        val speed = velocity.map { it * it }.sum().pow(0.5f).coerceIn(0f, 30f)
+        val acceleration = accMag.coerceIn(0f, 20f)
+
+        latestSpeed = speed
+
+        emitCombinedImuData(speed, acceleration, latestDirectionRad)
+
+        Log.d("IMU_SENSOR", "Accel x: %.3f y: %.3f z: %.3f | Speed: %.3f | AccelMag: %.3f | Dir: %.2f rad".format(
+            lastAccel[0], lastAccel[1], lastAccel[2], speed, acceleration, latestDirectionRad
+        ))
+    }
+
+    private fun handleGyroscope(event: SensorEvent) {
+        lastGyro = event.values.copyOf()
+    }
+
+    private fun handleRotationVector(event: SensorEvent) {
+        val rotationMatrix = FloatArray(9)
+        SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)
+        val orientation = FloatArray(3)
+        SensorManager.getOrientation(rotationMatrix, orientation)
+
+        val azimuth = orientation[0] // radian
+
+        // Apply smoothing to direction
+        val smoothFactor = 0.85f
+        if (abs(azimuth - latestDirectionRad) > 0.05f) {
+            latestDirectionRad = smoothFactor * latestDirectionRad + (1 - smoothFactor) * azimuth
+        }
+    }
+
+    private fun emitCombinedImuData(speed: Float, acceleration: Float, directionRad: Float) {
+        onImuUpdate(
+            ImuData(
+                accelX = lastAccel[0],
+                accelY = lastAccel[1],
+                accelZ = lastAccel[2],
+                gyroX = lastGyro[0],
+                gyroY = lastGyro[1],
+                gyroZ = lastGyro[2],
+                speed = speed,
+                acceleration = acceleration,
+                directionRad = directionRad
+            )
+        )
+    }
+
+    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
+        // Not used
+    }
+}
+
+data class ImuData(
+    val accelX: Float,
+    val accelY: Float,
+    val accelZ: Float,
+    val gyroX: Float,
+    val gyroY: Float,
+    val gyroZ: Float,
+    val speed: Float,
+    val acceleration: Float,
+    val directionRad: Float
+)
\ No newline at end of file
diff --git a/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager.kt b/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager.kt
new file mode 100644
index 0000000..bb0b261
--- /dev/null
+++ b/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager.kt
@@ -0,0 +1,163 @@
+package com.dumon.plugin.geolocation.wifi
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.wifi.ScanResult
+import android.net.wifi.WifiManager
+import android.net.wifi.rtt.RangingRequest
+import android.net.wifi.rtt.RangingResultCallback
+import android.net.wifi.rtt.WifiRttManager
+import android.os.*
+import android.util.Log
+import androidx.core.app.ActivityCompat
+import java.util.concurrent.Executors
+
+class WifiPositioningManager(
+    private val context: Context,
+    private val onWifiPositioningUpdate: (WifiScanResult) -> Unit
+) {
+
+    private val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
+    private val wifiRttManager: WifiRttManager? =
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            context.applicationContext.getSystemService(Context.WIFI_RTT_RANGING_SERVICE) as? WifiRttManager
+        } else {
+            null
+        }
+
+    private val handler = Handler(Looper.getMainLooper())
+    private var isScanning = false
+
+    private var lastWifiScanAps: MutableList = mutableListOf()
+
+    fun isRttSupported(): Boolean {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && wifiRttManager != null && wifiManager.isWifiEnabled
+    }
+
+    fun startPeriodicScan(intervalMs: Long = 3000L) {
+        isScanning = true
+        handler.post(object : Runnable {
+            override fun run() {
+                if (isScanning) {
+                    startWifiScan()
+                    handler.postDelayed(this, intervalMs)
+                }
+            }
+        })
+    }
+
+    fun stopPeriodicScan() {
+        isScanning = false
+        handler.removeCallbacksAndMessages(null)
+    }
+
+    fun startWifiScan() {
+        if (ActivityCompat.checkSelfPermission(
+                context,
+                Manifest.permission.ACCESS_FINE_LOCATION
+            ) == PackageManager.PERMISSION_GRANTED
+        ) {
+            val success = wifiManager.startScan()
+            if (success) {
+                val results = wifiManager.scanResults
+                processScanResults(results)
+                if (isRttSupported()) {
+                    startRttRanging(results)
+                }
+            } else {
+                Log.e("WIFI_POSITION", "Wi-Fi scan failed")
+                onWifiPositioningUpdate(WifiScanResult(0, emptyList()))
+            }
+        } else {
+            Log.e("WIFI_POSITION", "Missing ACCESS_FINE_LOCATION permission")
+            onWifiPositioningUpdate(WifiScanResult(0, emptyList()))
+        }
+    }
+
+    private fun processScanResults(results: List) {
+        lastWifiScanAps = results.map { result ->
+            WifiAp(
+                ssid = result.SSID,
+                bssid = result.BSSID,
+                rssi = result.level
+            )
+        }.toMutableList()
+
+        Log.d("WIFI_POSITION", "Wi-Fi scan โ†’ AP count: ${lastWifiScanAps.size}")
+        lastWifiScanAps.forEach {
+            Log.d("WIFI_POSITION", "SSID: ${it.ssid}, BSSID: ${it.bssid}, RSSI: ${it.rssi} dBm")
+        }
+
+        onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps))
+    }
+
+    private fun startRttRanging(results: List) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || wifiRttManager == null) {
+            Log.d("WIFI_POSITION", "RTT not supported or WifiRttManager is null.")
+            return
+        }
+
+        val rttCapableAps = results.filter { it.is80211mcResponder }
+        if (rttCapableAps.isEmpty()) {
+            Log.d("WIFI_POSITION", "No RTT-capable AP found.")
+            return
+        }
+
+        val rangingRequest = RangingRequest.Builder()
+            .addAccessPoints(rttCapableAps)
+            .build()
+
+        try {
+            wifiRttManager.startRanging(
+                rangingRequest,
+                Executors.newSingleThreadExecutor(),
+                object : RangingResultCallback() {
+                    override fun onRangingFailure(code: Int) {
+                        Log.e("WIFI_POSITION", "RTT Ranging failed: $code")
+                    }
+
+                    override fun onRangingResults(results: List) {
+                        results.forEach { result ->
+                            val mac = result.macAddress.toString()
+                            val distance = if (result.status == android.net.wifi.rtt.RangingResult.STATUS_SUCCESS) {
+                                result.distanceMm / 1000.0
+                            } else {
+                                Log.w("WIFI_POSITION", "RTT distance unavailable for ${result.macAddress}")
+                                null
+                            }
+
+                            lastWifiScanAps.indexOfFirst { it.bssid == mac }.takeIf { it >= 0 }?.let { idx ->
+                                lastWifiScanAps[idx] = lastWifiScanAps[idx].copy(distance = distance)
+                            }
+
+                            Log.d(
+                                "WIFI_POSITION",
+                                if (distance != null)
+                                    "RTT โ†’ ${mac}, Distance: ${distance} m"
+                                else
+                                    "RTT โ†’ ${mac}, Status: ${result.status}"
+                            )
+                        }
+
+                        onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps))
+                    }
+                }
+            )
+        } catch (e: SecurityException) {
+            Log.e("WIFI_POSITION", "SecurityException: Missing NEARBY_WIFI_DEVICES permission", e)
+        }
+    }
+}
+
+data class WifiAp(
+    val ssid: String,
+    val bssid: String,
+    val rssi: Int,
+    val distance: Double? = null
+)
+
+data class WifiScanResult(
+    val apCount: Int,
+    val aps: List
+)
\ No newline at end of file
diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml
new file mode 100644
index 0000000..f8fa0ec
--- /dev/null
+++ b/android/src/main/res/values/strings.xml
@@ -0,0 +1,6 @@
+
+    DumonGeolocation
+    Akses lokasi belum diberikan
+    Pelacakan dimulai
+    Pelacakan dihentikan
+
\ No newline at end of file
diff --git a/example-app/android/.gitignore b/example-app/android/.gitignore
new file mode 100644
index 0000000..48354a3
--- /dev/null
+++ b/example-app/android/.gitignore
@@ -0,0 +1,101 @@
+# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
+
+# Built application files
+*.apk
+*.aar
+*.ap_
+*.aab
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+#  Uncomment the following line in case you need and you don't have the release build type files in your app
+# release/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# IntelliJ
+*.iml
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/gradle.xml
+.idea/assetWizardSettings.xml
+.idea/dictionaries
+.idea/libraries
+# Android Studio 3 in .gitignore file.
+.idea/caches
+.idea/modules.xml
+# Comment next line if keeping position of elements in Navigation Editor is relevant for you
+.idea/navEditor.xml
+
+# Keystore files
+# Uncomment the following lines if you do not want to check your keystore files in.
+#*.jks
+#*.keystore
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+.cxx/
+
+# Google Services (e.g. APIs or Firebase)
+# google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+# fastlane
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots
+fastlane/test_output
+fastlane/readme.md
+
+# Version control
+vcs.xml
+
+# lint
+lint/intermediates/
+lint/generated/
+lint/outputs/
+lint/tmp/
+# lint/reports/
+
+# Android Profiling
+*.hprof
+
+# Cordova plugins for Capacitor
+capacitor-cordova-android-plugins
+
+# Copied web assets
+app/src/main/assets/public
+
+# Generated Config files
+app/src/main/assets/capacitor.config.json
+app/src/main/assets/capacitor.plugins.json
+app/src/main/res/xml/config.xml
diff --git a/example-app/android/app/.gitignore b/example-app/android/app/.gitignore
new file mode 100644
index 0000000..043df80
--- /dev/null
+++ b/example-app/android/app/.gitignore
@@ -0,0 +1,2 @@
+/build/*
+!/build/.npmkeep
diff --git a/example-app/android/app/build.gradle b/example-app/android/app/build.gradle
new file mode 100644
index 0000000..4d1354d
--- /dev/null
+++ b/example-app/android/app/build.gradle
@@ -0,0 +1,54 @@
+apply plugin: 'com.android.application'
+
+android {
+    namespace "com.example.plugin"
+    compileSdk rootProject.ext.compileSdkVersion
+    defaultConfig {
+        applicationId "com.example.plugin"
+        minSdkVersion rootProject.ext.minSdkVersion
+        targetSdkVersion rootProject.ext.targetSdkVersion
+        versionCode 1
+        versionName "1.0"
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        aaptOptions {
+             // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+             // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
+            ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
+        }
+    }
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+    }
+}
+
+repositories {
+    flatDir{
+        dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
+    }
+}
+
+dependencies {
+    implementation fileTree(include: ['*.jar'], dir: 'libs')
+    implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
+    implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
+    implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
+    implementation project(':capacitor-android')
+    testImplementation "junit:junit:$junitVersion"
+    androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
+    androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
+    implementation project(':capacitor-cordova-android-plugins')
+}
+
+apply from: 'capacitor.build.gradle'
+
+try {
+    def servicesJSON = file('google-services.json')
+    if (servicesJSON.text) {
+        apply plugin: 'com.google.gms.google-services'
+    }
+} catch(Exception e) {
+    logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
+}
diff --git a/example-app/android/app/capacitor.build.gradle b/example-app/android/app/capacitor.build.gradle
new file mode 100644
index 0000000..28347c1
--- /dev/null
+++ b/example-app/android/app/capacitor.build.gradle
@@ -0,0 +1,19 @@
+// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
+
+android {
+  compileOptions {
+      sourceCompatibility JavaVersion.VERSION_21
+      targetCompatibility JavaVersion.VERSION_21
+  }
+}
+
+apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
+dependencies {
+    implementation project(':dumon-geolocation')
+
+}
+
+
+if (hasProperty('postBuildExtras')) {
+  postBuildExtras()
+}
diff --git a/example-app/android/app/proguard-rules.pro b/example-app/android/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/example-app/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/example-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/example-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..f2c2217
--- /dev/null
+++ b/example-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.getcapacitor.myapp;
+
+import static org.junit.Assert.*;
+
+import android.content.Context;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+
+    @Test
+    public void useAppContext() throws Exception {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+        assertEquals("com.getcapacitor.app", appContext.getPackageName());
+    }
+}
diff --git a/example-app/android/app/src/main/AndroidManifest.xml b/example-app/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9d1f585
--- /dev/null
+++ b/example-app/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,49 @@
+
+
+
+    
+
+        
+
+            
+                
+                
+            
+
+        
+
+        
+            
+        
+    
+
+    
+
+    
+    
+    
+    
+    
+    
+    
+    
+    
+
diff --git a/example-app/android/app/src/main/java/com/example/plugin/MainActivity.java b/example-app/android/app/src/main/java/com/example/plugin/MainActivity.java
new file mode 100644
index 0000000..9aa7b47
--- /dev/null
+++ b/example-app/android/app/src/main/java/com/example/plugin/MainActivity.java
@@ -0,0 +1,5 @@
+package com.example.plugin;
+
+import com.getcapacitor.BridgeActivity;
+
+public class MainActivity extends BridgeActivity {}
diff --git a/example-app/android/app/src/main/res/drawable-land-hdpi/splash.png b/example-app/android/app/src/main/res/drawable-land-hdpi/splash.png
new file mode 100644
index 0000000..e31573b
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-land-hdpi/splash.png differ
diff --git a/example-app/android/app/src/main/res/drawable-land-mdpi/splash.png b/example-app/android/app/src/main/res/drawable-land-mdpi/splash.png
new file mode 100644
index 0000000..f7a6492
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-land-mdpi/splash.png differ
diff --git a/example-app/android/app/src/main/res/drawable-land-xhdpi/splash.png b/example-app/android/app/src/main/res/drawable-land-xhdpi/splash.png
new file mode 100644
index 0000000..8077255
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-land-xhdpi/splash.png differ
diff --git a/example-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/example-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png
new file mode 100644
index 0000000..14c6c8f
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ
diff --git a/example-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/example-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png
new file mode 100644
index 0000000..244ca25
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ
diff --git a/example-app/android/app/src/main/res/drawable-port-hdpi/splash.png b/example-app/android/app/src/main/res/drawable-port-hdpi/splash.png
new file mode 100644
index 0000000..74faaa5
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-port-hdpi/splash.png differ
diff --git a/example-app/android/app/src/main/res/drawable-port-mdpi/splash.png b/example-app/android/app/src/main/res/drawable-port-mdpi/splash.png
new file mode 100644
index 0000000..e944f4a
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-port-mdpi/splash.png differ
diff --git a/example-app/android/app/src/main/res/drawable-port-xhdpi/splash.png b/example-app/android/app/src/main/res/drawable-port-xhdpi/splash.png
new file mode 100644
index 0000000..564a82f
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-port-xhdpi/splash.png differ
diff --git a/example-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/example-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png
new file mode 100644
index 0000000..bfabe68
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ
diff --git a/example-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/example-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png
new file mode 100644
index 0000000..6929071
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ
diff --git a/example-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/example-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..c7bd21d
--- /dev/null
+++ b/example-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+    
+        
+            
+                
+                
+            
+        
+    
+    
+
diff --git a/example-app/android/app/src/main/res/drawable/ic_launcher_background.xml b/example-app/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..d5fccc5
--- /dev/null
+++ b/example-app/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+
diff --git a/example-app/android/app/src/main/res/drawable/splash.png b/example-app/android/app/src/main/res/drawable/splash.png
new file mode 100644
index 0000000..f7a6492
Binary files /dev/null and b/example-app/android/app/src/main/res/drawable/splash.png differ
diff --git a/example-app/android/app/src/main/res/layout/activity_main.xml b/example-app/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..b5ad138
--- /dev/null
+++ b/example-app/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,12 @@
+
+
+
+    
+
diff --git a/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+    
+    
+
\ No newline at end of file
diff --git a/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..036d09b
--- /dev/null
+++ b/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+    
+    
+
\ No newline at end of file
diff --git a/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..c023e50
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..2127973
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b441f37
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..72905b8
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..8ed0605
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9502e47
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d1e077
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..df0f158
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..853db04
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..6cdf97c
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..2960cbb
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e3093a
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..46de6e2
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..d2ea9ab
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..a40d73e
Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/example-app/android/app/src/main/res/values/ic_launcher_background.xml b/example-app/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..c5d5899
--- /dev/null
+++ b/example-app/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+    #FFFFFF
+
\ No newline at end of file
diff --git a/example-app/android/app/src/main/res/values/strings.xml b/example-app/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..59ba8ea
--- /dev/null
+++ b/example-app/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,7 @@
+
+
+    example-app
+    example-app
+    com.example.plugin
+    com.example.plugin
+
diff --git a/example-app/android/app/src/main/res/values/styles.xml b/example-app/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..be874e5
--- /dev/null
+++ b/example-app/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+    
+    
+
+    
+
+
+    
+
\ No newline at end of file
diff --git a/example-app/android/app/src/main/res/xml/file_paths.xml b/example-app/android/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..bd0c4d8
--- /dev/null
+++ b/example-app/android/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,5 @@
+
+
+    
+    
+
\ No newline at end of file
diff --git a/example-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/example-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
new file mode 100644
index 0000000..0297327
--- /dev/null
+++ b/example-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
@@ -0,0 +1,18 @@
+package com.getcapacitor.myapp;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+
+    @Test
+    public void addition_isCorrect() throws Exception {
+        assertEquals(4, 2 + 2);
+    }
+}
diff --git a/example-app/android/build.gradle b/example-app/android/build.gradle
new file mode 100644
index 0000000..f1b3b0e
--- /dev/null
+++ b/example-app/android/build.gradle
@@ -0,0 +1,29 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+    
+    repositories {
+        google()
+        mavenCentral()
+    }
+    dependencies {
+        classpath 'com.android.tools.build:gradle:8.7.2'
+        classpath 'com.google.gms:google-services:4.4.2'
+
+        // NOTE: Do not place your application dependencies here; they belong
+        // in the individual module build.gradle files
+    }
+}
+
+apply from: "variables.gradle"
+
+allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}
diff --git a/example-app/android/capacitor.settings.gradle b/example-app/android/capacitor.settings.gradle
new file mode 100644
index 0000000..91e5de6
--- /dev/null
+++ b/example-app/android/capacitor.settings.gradle
@@ -0,0 +1,6 @@
+// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
+include ':capacitor-android'
+project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
+
+include ':dumon-geolocation'
+project(':dumon-geolocation').projectDir = new File('../../android')
diff --git a/example-app/android/gradle.properties b/example-app/android/gradle.properties
new file mode 100644
index 0000000..2e87c52
--- /dev/null
+++ b/example-app/android/gradle.properties
@@ -0,0 +1,22 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
diff --git a/example-app/android/gradle/wrapper/gradle-wrapper.jar b/example-app/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..a4b76b9
Binary files /dev/null and b/example-app/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/example-app/android/gradle/wrapper/gradle-wrapper.properties b/example-app/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..c1d5e01
--- /dev/null
+++ b/example-app/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/example-app/android/gradlew b/example-app/android/gradlew
new file mode 100755
index 0000000..f5feea6
--- /dev/null
+++ b/example-app/android/gradlew
@@ -0,0 +1,252 @@
+#!/bin/sh
+
+#
+# Copyright ยฉ 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions ยซ$varยป, ยซ${var}ยป, ยซ${var:-default}ยป, ยซ${var+SET}ยป,
+#           ยซ${var#prefix}ยป, ยซ${var%suffix}ยป, and ยซ$( cmd )ยป;
+#         * compound commands having a testable exit status, especially ยซcaseยป;
+#         * various built-in commands including ยซcommandยป, ยซsetยป, and ยซulimitยป.
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    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
+' "$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, 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/example-app/android/gradlew.bat b/example-app/android/gradlew.bat
new file mode 100644
index 0000000..9b42019
--- /dev/null
+++ b/example-app/android/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
diff --git a/example-app/android/settings.gradle b/example-app/android/settings.gradle
new file mode 100644
index 0000000..3b4431d
--- /dev/null
+++ b/example-app/android/settings.gradle
@@ -0,0 +1,5 @@
+include ':app'
+include ':capacitor-cordova-android-plugins'
+project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
+
+apply from: 'capacitor.settings.gradle'
\ No newline at end of file
diff --git a/example-app/android/variables.gradle b/example-app/android/variables.gradle
new file mode 100644
index 0000000..2c8e408
--- /dev/null
+++ b/example-app/android/variables.gradle
@@ -0,0 +1,16 @@
+ext {
+    minSdkVersion = 23
+    compileSdkVersion = 35
+    targetSdkVersion = 35
+    androidxActivityVersion = '1.9.2'
+    androidxAppCompatVersion = '1.7.0'
+    androidxCoordinatorLayoutVersion = '1.2.0'
+    androidxCoreVersion = '1.15.0'
+    androidxFragmentVersion = '1.8.4'
+    coreSplashScreenVersion = '1.0.1'
+    androidxWebkitVersion = '1.12.1'
+    junitVersion = '4.13.2'
+    androidxJunitVersion = '1.2.1'
+    androidxEspressoCoreVersion = '3.6.1'
+    cordovaAndroidVersion = '10.1.1'
+}
\ No newline at end of file
diff --git a/example-app/package-lock.json b/example-app/package-lock.json
new file mode 100644
index 0000000..84ab05b
--- /dev/null
+++ b/example-app/package-lock.json
@@ -0,0 +1,5600 @@
+{
+  "name": "capacitor-app",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "capacitor-app",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "@capacitor/android": "7.0.0",
+        "@capacitor/core": "latest",
+        "@capacitor/ios": "7.0.0",
+        "dumon-geolocation": "file:.."
+      },
+      "devDependencies": {
+        "@capacitor/cli": "latest",
+        "vite": "^5.4.2"
+      }
+    },
+    "..": {
+      "version": "0.0.1",
+      "license": "MIT",
+      "devDependencies": {
+        "@capacitor/android": "^7.0.0",
+        "@capacitor/core": "^7.0.0",
+        "@capacitor/docgen": "^0.3.0",
+        "@capacitor/ios": "^7.0.0",
+        "@ionic/eslint-config": "^0.4.0",
+        "@ionic/prettier-config": "^4.0.0",
+        "@ionic/swiftlint-config": "^2.0.0",
+        "eslint": "^8.57.0",
+        "prettier": "^3.4.2",
+        "prettier-plugin-java": "^2.6.6",
+        "rimraf": "^6.0.1",
+        "rollup": "^4.30.1",
+        "swiftlint": "^2.0.0",
+        "typescript": "~4.1.5"
+      },
+      "peerDependencies": {
+        "@capacitor/core": ">=7.0.0"
+      }
+    },
+    "../node_modules/@babel/code-frame": {
+      "version": "7.27.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-validator-identifier": "^7.27.1",
+        "js-tokens": "^4.0.0",
+        "picocolors": "^1.1.1"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "../node_modules/@babel/helper-validator-identifier": {
+      "version": "7.27.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "../node_modules/@capacitor/android": {
+      "version": "7.3.0",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": "^7.3.0"
+      }
+    },
+    "../node_modules/@capacitor/core": {
+      "version": "7.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.1.0"
+      }
+    },
+    "../node_modules/@capacitor/docgen": {
+      "version": "0.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "^14.18.0",
+        "colorette": "^2.0.20",
+        "github-slugger": "^1.5.0",
+        "minimist": "^1.2.8",
+        "typescript": "~4.2.4"
+      },
+      "bin": {
+        "docgen": "bin/docgen"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "../node_modules/@capacitor/docgen/node_modules/typescript": {
+      "version": "4.2.4",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=4.2.0"
+      }
+    },
+    "../node_modules/@capacitor/ios": {
+      "version": "7.3.0",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": "^7.3.0"
+      }
+    },
+    "../node_modules/@chevrotain/cst-dts-gen": {
+      "version": "11.0.3",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@chevrotain/gast": "11.0.3",
+        "@chevrotain/types": "11.0.3",
+        "lodash-es": "4.17.21"
+      }
+    },
+    "../node_modules/@chevrotain/gast": {
+      "version": "11.0.3",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@chevrotain/types": "11.0.3",
+        "lodash-es": "4.17.21"
+      }
+    },
+    "../node_modules/@chevrotain/regexp-to-ast": {
+      "version": "11.0.3",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "../node_modules/@chevrotain/types": {
+      "version": "11.0.3",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "../node_modules/@chevrotain/utils": {
+      "version": "11.0.3",
+      "dev": true,
+      "license": "Apache-2.0"
+    },
+    "../node_modules/@eslint-community/eslint-utils": {
+      "version": "4.7.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "../node_modules/@eslint-community/regexpp": {
+      "version": "4.12.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "../node_modules/@eslint/eslintrc": {
+      "version": "2.1.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^9.6.0",
+        "globals": "^13.19.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "../node_modules/@eslint/js": {
+      "version": "8.57.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      }
+    },
+    "../node_modules/@humanwhocodes/config-array": {
+      "version": "0.13.0",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanwhocodes/object-schema": "^2.0.3",
+        "debug": "^4.3.1",
+        "minimatch": "^3.0.5"
+      },
+      "engines": {
+        "node": ">=10.10.0"
+      }
+    },
+    "../node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "../node_modules/@humanwhocodes/object-schema": {
+      "version": "2.0.3",
+      "dev": true,
+      "license": "BSD-3-Clause"
+    },
+    "../node_modules/@ionic/eslint-config": {
+      "version": "0.4.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/eslint-plugin": "^5.58.0",
+        "@typescript-eslint/parser": "^5.58.0",
+        "eslint-config-prettier": "^8.8.0",
+        "eslint-plugin-import": "^2.27.0"
+      },
+      "peerDependencies": {
+        "eslint": ">=7"
+      }
+    },
+    "../node_modules/@ionic/prettier-config": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "prettier": "^2.4.0 || ^3.0.0"
+      }
+    },
+    "../node_modules/@ionic/swiftlint-config": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/@ionic/utils-array": {
+      "version": "2.1.6",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "../node_modules/@ionic/utils-fs": {
+      "version": "3.1.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/fs-extra": "^8.0.0",
+        "debug": "^4.0.0",
+        "fs-extra": "^9.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "../node_modules/@ionic/utils-object": {
+      "version": "2.1.6",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "../node_modules/@ionic/utils-process": {
+      "version": "2.1.12",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-object": "2.1.6",
+        "@ionic/utils-terminal": "2.3.5",
+        "debug": "^4.0.0",
+        "signal-exit": "^3.0.3",
+        "tree-kill": "^1.2.2",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "../node_modules/@ionic/utils-process/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/@ionic/utils-stream": {
+      "version": "3.1.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "../node_modules/@ionic/utils-subprocess": {
+      "version": "3.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-array": "2.1.6",
+        "@ionic/utils-fs": "3.1.7",
+        "@ionic/utils-process": "2.1.12",
+        "@ionic/utils-stream": "3.1.7",
+        "@ionic/utils-terminal": "2.3.5",
+        "cross-spawn": "^7.0.3",
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "../node_modules/@ionic/utils-terminal": {
+      "version": "2.3.5",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/slice-ansi": "^4.0.0",
+        "debug": "^4.0.0",
+        "signal-exit": "^3.0.3",
+        "slice-ansi": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0",
+        "tslib": "^2.0.1",
+        "untildify": "^4.0.0",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "../node_modules/@ionic/utils-terminal/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/@ionic/utils-terminal/node_modules/signal-exit": {
+      "version": "3.0.7",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/@ionic/utils-terminal/node_modules/string-width": {
+      "version": "4.2.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/@ionic/utils-terminal/node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "../node_modules/@isaacs/balanced-match": {
+      "version": "4.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "../node_modules/@isaacs/brace-expansion": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@isaacs/balanced-match": "^4.0.1"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "../node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "../node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "../node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "../node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "../node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "../node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "../node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.43.0",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "../node_modules/@rtsao/scc": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/@types/estree": {
+      "version": "1.0.7",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/@types/fs-extra": {
+      "version": "8.1.5",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "../node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/@types/json5": {
+      "version": "0.0.29",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/@types/node": {
+      "version": "14.18.63",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/@types/semver": {
+      "version": "7.7.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/@types/slice-ansi": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "5.62.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.4.0",
+        "@typescript-eslint/scope-manager": "5.62.0",
+        "@typescript-eslint/type-utils": "5.62.0",
+        "@typescript-eslint/utils": "5.62.0",
+        "debug": "^4.3.4",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.0",
+        "natural-compare-lite": "^1.4.0",
+        "semver": "^7.3.7",
+        "tsutils": "^3.21.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^5.0.0",
+        "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "../node_modules/@typescript-eslint/parser": {
+      "version": "5.62.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "5.62.0",
+        "@typescript-eslint/types": "5.62.0",
+        "@typescript-eslint/typescript-estree": "5.62.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "../node_modules/@typescript-eslint/scope-manager": {
+      "version": "5.62.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "5.62.0",
+        "@typescript-eslint/visitor-keys": "5.62.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "../node_modules/@typescript-eslint/type-utils": {
+      "version": "5.62.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/typescript-estree": "5.62.0",
+        "@typescript-eslint/utils": "5.62.0",
+        "debug": "^4.3.4",
+        "tsutils": "^3.21.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "*"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "../node_modules/@typescript-eslint/types": {
+      "version": "5.62.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "../node_modules/@typescript-eslint/typescript-estree": {
+      "version": "5.62.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "@typescript-eslint/types": "5.62.0",
+        "@typescript-eslint/visitor-keys": "5.62.0",
+        "debug": "^4.3.4",
+        "globby": "^11.1.0",
+        "is-glob": "^4.0.3",
+        "semver": "^7.3.7",
+        "tsutils": "^3.21.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "../node_modules/@typescript-eslint/utils": {
+      "version": "5.62.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@types/json-schema": "^7.0.9",
+        "@types/semver": "^7.3.12",
+        "@typescript-eslint/scope-manager": "5.62.0",
+        "@typescript-eslint/types": "5.62.0",
+        "@typescript-eslint/typescript-estree": "5.62.0",
+        "eslint-scope": "^5.1.1",
+        "semver": "^7.3.7"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "../node_modules/@typescript-eslint/visitor-keys": {
+      "version": "5.62.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "5.62.0",
+        "eslint-visitor-keys": "^3.3.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "../node_modules/@ungap/structured-clone": {
+      "version": "1.3.0",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/acorn": {
+      "version": "8.15.0",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "../node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "../node_modules/ajv": {
+      "version": "6.12.6",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "../node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "../node_modules/argparse": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "Python-2.0"
+    },
+    "../node_modules/array-buffer-byte-length": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "is-array-buffer": "^3.0.5"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/array-includes": {
+      "version": "3.1.9",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.24.0",
+        "es-object-atoms": "^1.1.1",
+        "get-intrinsic": "^1.3.0",
+        "is-string": "^1.1.1",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/array-union": {
+      "version": "2.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/array.prototype.findlastindex": {
+      "version": "1.2.6",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.9",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "es-shim-unscopables": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/array.prototype.flat": {
+      "version": "1.3.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/array.prototype.flatmap": {
+      "version": "1.3.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-shim-unscopables": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/arraybuffer.prototype.slice": {
+      "version": "1.0.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array-buffer-byte-length": "^1.0.1",
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "is-array-buffer": "^3.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/astral-regex": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/async-function": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/at-least-node": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "../node_modules/available-typed-arrays": {
+      "version": "1.0.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "possible-typed-array-names": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/balanced-match": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "../node_modules/braces": {
+      "version": "3.0.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/call-bind": {
+      "version": "1.0.8",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.0",
+        "es-define-property": "^1.0.0",
+        "get-intrinsic": "^1.2.4",
+        "set-function-length": "^1.2.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/call-bound": {
+      "version": "1.0.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "get-intrinsic": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/callsites": {
+      "version": "3.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "../node_modules/chalk": {
+      "version": "4.1.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "../node_modules/chevrotain": {
+      "version": "11.0.3",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@chevrotain/cst-dts-gen": "11.0.3",
+        "@chevrotain/gast": "11.0.3",
+        "@chevrotain/regexp-to-ast": "11.0.3",
+        "@chevrotain/types": "11.0.3",
+        "@chevrotain/utils": "11.0.3",
+        "lodash-es": "4.17.21"
+      }
+    },
+    "../node_modules/chevrotain-allstar": {
+      "version": "0.3.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "lodash-es": "^4.17.21"
+      },
+      "peerDependencies": {
+        "chevrotain": "^11.0.0"
+      }
+    },
+    "../node_modules/color-convert": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "../node_modules/color-name": {
+      "version": "1.1.4",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/colorette": {
+      "version": "2.0.20",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/concat-map": {
+      "version": "0.0.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "../node_modules/data-view-buffer": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/data-view-byte-length": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/inspect-js"
+      }
+    },
+    "../node_modules/data-view-byte-offset": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-data-view": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/debug": {
+      "version": "4.4.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "../node_modules/deep-is": {
+      "version": "0.1.4",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/define-data-property": {
+      "version": "1.1.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/define-properties": {
+      "version": "1.2.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.0.1",
+        "has-property-descriptors": "^1.0.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/dir-glob": {
+      "version": "3.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-type": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/doctrine": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "../node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/env-paths": {
+      "version": "2.2.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "../node_modules/error-ex": {
+      "version": "1.3.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-arrayish": "^0.2.1"
+      }
+    },
+    "../node_modules/es-abstract": {
+      "version": "1.24.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array-buffer-byte-length": "^1.0.2",
+        "arraybuffer.prototype.slice": "^1.0.4",
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "data-view-buffer": "^1.0.2",
+        "data-view-byte-length": "^1.0.2",
+        "data-view-byte-offset": "^1.0.1",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "es-set-tostringtag": "^2.1.0",
+        "es-to-primitive": "^1.3.0",
+        "function.prototype.name": "^1.1.8",
+        "get-intrinsic": "^1.3.0",
+        "get-proto": "^1.0.1",
+        "get-symbol-description": "^1.1.0",
+        "globalthis": "^1.0.4",
+        "gopd": "^1.2.0",
+        "has-property-descriptors": "^1.0.2",
+        "has-proto": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "internal-slot": "^1.1.0",
+        "is-array-buffer": "^3.0.5",
+        "is-callable": "^1.2.7",
+        "is-data-view": "^1.0.2",
+        "is-negative-zero": "^2.0.3",
+        "is-regex": "^1.2.1",
+        "is-set": "^2.0.3",
+        "is-shared-array-buffer": "^1.0.4",
+        "is-string": "^1.1.1",
+        "is-typed-array": "^1.1.15",
+        "is-weakref": "^1.1.1",
+        "math-intrinsics": "^1.1.0",
+        "object-inspect": "^1.13.4",
+        "object-keys": "^1.1.1",
+        "object.assign": "^4.1.7",
+        "own-keys": "^1.0.1",
+        "regexp.prototype.flags": "^1.5.4",
+        "safe-array-concat": "^1.1.3",
+        "safe-push-apply": "^1.0.0",
+        "safe-regex-test": "^1.1.0",
+        "set-proto": "^1.0.0",
+        "stop-iteration-iterator": "^1.1.0",
+        "string.prototype.trim": "^1.2.10",
+        "string.prototype.trimend": "^1.0.9",
+        "string.prototype.trimstart": "^1.0.8",
+        "typed-array-buffer": "^1.0.3",
+        "typed-array-byte-length": "^1.0.3",
+        "typed-array-byte-offset": "^1.0.4",
+        "typed-array-length": "^1.0.7",
+        "unbox-primitive": "^1.1.0",
+        "which-typed-array": "^1.1.19"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/es-define-property": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/es-errors": {
+      "version": "1.3.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/es-shim-unscopables": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/es-to-primitive": {
+      "version": "1.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-callable": "^1.2.7",
+        "is-date-object": "^1.0.5",
+        "is-symbol": "^1.0.4"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/eslint": {
+      "version": "8.57.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.2.0",
+        "@eslint-community/regexpp": "^4.6.1",
+        "@eslint/eslintrc": "^2.1.4",
+        "@eslint/js": "8.57.1",
+        "@humanwhocodes/config-array": "^0.13.0",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@nodelib/fs.walk": "^1.2.8",
+        "@ungap/structured-clone": "^1.2.0",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.2",
+        "debug": "^4.3.2",
+        "doctrine": "^3.0.0",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^7.2.2",
+        "eslint-visitor-keys": "^3.4.3",
+        "espree": "^9.6.1",
+        "esquery": "^1.4.2",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^6.0.1",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "globals": "^13.19.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "is-path-inside": "^3.0.3",
+        "js-yaml": "^4.1.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "levn": "^0.4.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3",
+        "strip-ansi": "^6.0.1",
+        "text-table": "^0.2.0"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "../node_modules/eslint-config-prettier": {
+      "version": "8.10.0",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "eslint-config-prettier": "bin/cli.js"
+      },
+      "peerDependencies": {
+        "eslint": ">=7.0.0"
+      }
+    },
+    "../node_modules/eslint-import-resolver-node": {
+      "version": "0.3.9",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^3.2.7",
+        "is-core-module": "^2.13.0",
+        "resolve": "^1.22.4"
+      }
+    },
+    "../node_modules/eslint-import-resolver-node/node_modules/debug": {
+      "version": "3.2.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "../node_modules/eslint-module-utils": {
+      "version": "2.12.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^3.2.7"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependenciesMeta": {
+        "eslint": {
+          "optional": true
+        }
+      }
+    },
+    "../node_modules/eslint-module-utils/node_modules/debug": {
+      "version": "3.2.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "../node_modules/eslint-plugin-import": {
+      "version": "2.31.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@rtsao/scc": "^1.1.0",
+        "array-includes": "^3.1.8",
+        "array.prototype.findlastindex": "^1.2.5",
+        "array.prototype.flat": "^1.3.2",
+        "array.prototype.flatmap": "^1.3.2",
+        "debug": "^3.2.7",
+        "doctrine": "^2.1.0",
+        "eslint-import-resolver-node": "^0.3.9",
+        "eslint-module-utils": "^2.12.0",
+        "hasown": "^2.0.2",
+        "is-core-module": "^2.15.1",
+        "is-glob": "^4.0.3",
+        "minimatch": "^3.1.2",
+        "object.fromentries": "^2.0.8",
+        "object.groupby": "^1.0.3",
+        "object.values": "^1.2.0",
+        "semver": "^6.3.1",
+        "string.prototype.trimend": "^1.0.8",
+        "tsconfig-paths": "^3.15.0"
+      },
+      "engines": {
+        "node": ">=4"
+      },
+      "peerDependencies": {
+        "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+      }
+    },
+    "../node_modules/eslint-plugin-import/node_modules/debug": {
+      "version": "3.2.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.1"
+      }
+    },
+    "../node_modules/eslint-plugin-import/node_modules/doctrine": {
+      "version": "2.1.0",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "esutils": "^2.0.2"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "../node_modules/eslint-plugin-import/node_modules/semver": {
+      "version": "6.3.1",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
+    "../node_modules/eslint-scope": {
+      "version": "5.1.1",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^4.1.1"
+      },
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "../node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "../node_modules/eslint/node_modules/eslint-scope": {
+      "version": "7.2.2",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "../node_modules/eslint/node_modules/estraverse": {
+      "version": "5.3.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "../node_modules/espree": {
+      "version": "9.6.1",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.9.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^3.4.1"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "../node_modules/esquery": {
+      "version": "1.6.0",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "../node_modules/esquery/node_modules/estraverse": {
+      "version": "5.3.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "../node_modules/esrecurse": {
+      "version": "4.3.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "../node_modules/esrecurse/node_modules/estraverse": {
+      "version": "5.3.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "../node_modules/estraverse": {
+      "version": "4.3.0",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "../node_modules/esutils": {
+      "version": "2.0.3",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "../node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/fast-glob": {
+      "version": "3.3.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "../node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "../node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/fastq": {
+      "version": "1.19.1",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "../node_modules/file-entry-cache": {
+      "version": "6.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^3.0.4"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "../node_modules/fill-range": {
+      "version": "7.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/find-up": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/flat-cache": {
+      "version": "3.2.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.3",
+        "rimraf": "^3.0.2"
+      },
+      "engines": {
+        "node": "^10.12.0 || >=12.0.0"
+      }
+    },
+    "../node_modules/flat-cache/node_modules/glob": {
+      "version": "7.2.3",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.1.1",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "../node_modules/flat-cache/node_modules/rimraf": {
+      "version": "3.0.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^7.1.3"
+      },
+      "bin": {
+        "rimraf": "bin.js"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "../node_modules/flatted": {
+      "version": "3.3.3",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/for-each": {
+      "version": "0.3.5",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-callable": "^1.2.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/foreground-child": {
+      "version": "3.3.1",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "../node_modules/fs-extra": {
+      "version": "9.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "at-least-node": "^1.0.0",
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "../node_modules/fs.realpath": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/function-bind": {
+      "version": "1.1.2",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/function.prototype.name": {
+      "version": "1.1.8",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "functions-have-names": "^1.2.3",
+        "hasown": "^2.0.2",
+        "is-callable": "^1.2.7"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/functions-have-names": {
+      "version": "1.2.3",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/get-proto": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/get-symbol-description": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/github-slugger": {
+      "version": "1.5.0",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/glob": {
+      "version": "11.0.3",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.3.1",
+        "jackspeak": "^4.1.1",
+        "minimatch": "^10.0.3",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^2.0.0"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "../node_modules/glob-parent": {
+      "version": "6.0.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "../node_modules/glob/node_modules/minimatch": {
+      "version": "10.0.3",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@isaacs/brace-expansion": "^5.0.0"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "../node_modules/globals": {
+      "version": "13.24.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "type-fest": "^0.20.2"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/globalthis": {
+      "version": "1.0.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-properties": "^1.2.1",
+        "gopd": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/globby": {
+      "version": "11.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "array-union": "^2.1.0",
+        "dir-glob": "^3.0.1",
+        "fast-glob": "^3.2.9",
+        "ignore": "^5.2.0",
+        "merge2": "^1.4.1",
+        "slash": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/gopd": {
+      "version": "1.2.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/graphemer": {
+      "version": "1.4.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/has-bigints": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/has-flag": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/has-property-descriptors": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-define-property": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/has-proto": {
+      "version": "1.2.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/has-symbols": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/hasown": {
+      "version": "2.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/ignore": {
+      "version": "5.3.2",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "../node_modules/import-fresh": {
+      "version": "3.3.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "../node_modules/inflight": {
+      "version": "1.0.6",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "../node_modules/inherits": {
+      "version": "2.0.4",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/internal-slot": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "hasown": "^2.0.2",
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/is-array-buffer": {
+      "version": "3.0.5",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-arrayish": {
+      "version": "0.2.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/is-async-function": {
+      "version": "2.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "async-function": "^1.0.0",
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.1",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-bigint": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-bigints": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-boolean-object": {
+      "version": "1.2.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-callable": {
+      "version": "1.2.7",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-core-module": {
+      "version": "2.16.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-data-view": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
+        "is-typed-array": "^1.1.13"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-date-object": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-extglob": {
+      "version": "2.1.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "../node_modules/is-finalizationregistry": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/is-generator-function": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "get-proto": "^1.0.0",
+        "has-tostringtag": "^1.0.2",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-glob": {
+      "version": "4.0.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "../node_modules/is-map": {
+      "version": "2.0.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-negative-zero": {
+      "version": "2.0.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-number": {
+      "version": "7.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "../node_modules/is-number-object": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-path-inside": {
+      "version": "3.0.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/is-regex": {
+      "version": "1.2.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-set": {
+      "version": "2.0.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-shared-array-buffer": {
+      "version": "1.0.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-string": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-symbol": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "has-symbols": "^1.1.0",
+        "safe-regex-test": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-typed-array": {
+      "version": "1.1.15",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "which-typed-array": "^1.1.16"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-weakmap": {
+      "version": "2.0.2",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-weakref": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/is-weakset": {
+      "version": "2.0.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "get-intrinsic": "^1.2.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/isarray": {
+      "version": "2.0.5",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/isexe": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/jackspeak": {
+      "version": "4.1.1",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "../node_modules/java-parser": {
+      "version": "2.3.4",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "chevrotain": "11.0.3",
+        "chevrotain-allstar": "0.3.1",
+        "lodash": "4.17.21"
+      }
+    },
+    "../node_modules/js-tokens": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/js-yaml": {
+      "version": "4.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "../node_modules/json-buffer": {
+      "version": "3.0.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/json-parse-even-better-errors": {
+      "version": "2.3.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/json5": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minimist": "^1.2.0"
+      },
+      "bin": {
+        "json5": "lib/cli.js"
+      }
+    },
+    "../node_modules/jsonfile": {
+      "version": "6.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "../node_modules/keyv": {
+      "version": "4.5.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "../node_modules/levn": {
+      "version": "0.4.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "../node_modules/lines-and-columns": {
+      "version": "1.2.4",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/locate-path": {
+      "version": "6.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/lodash": {
+      "version": "4.17.21",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/lodash-es": {
+      "version": "4.17.21",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/lru-cache": {
+      "version": "11.1.0",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "../node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/merge2": {
+      "version": "1.4.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "../node_modules/micromatch": {
+      "version": "4.0.8",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "../node_modules/minimatch": {
+      "version": "3.1.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "../node_modules/minimist": {
+      "version": "1.2.8",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/minipass": {
+      "version": "7.1.2",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "../node_modules/ms": {
+      "version": "2.1.3",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/natural-compare": {
+      "version": "1.4.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/natural-compare-lite": {
+      "version": "1.4.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/object-inspect": {
+      "version": "1.13.4",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/object-keys": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/object.assign": {
+      "version": "4.1.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0",
+        "has-symbols": "^1.1.0",
+        "object-keys": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/object.fromentries": {
+      "version": "2.0.8",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/object.groupby": {
+      "version": "1.0.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/object.values": {
+      "version": "1.2.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.3",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/once": {
+      "version": "1.4.0",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "../node_modules/optionator": {
+      "version": "0.9.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "../node_modules/own-keys": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "get-intrinsic": "^1.2.6",
+        "object-keys": "^1.1.1",
+        "safe-push-apply": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/p-limit": {
+      "version": "3.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/p-locate": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
+    },
+    "../node_modules/parent-module": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "../node_modules/parse-json": {
+      "version": "5.2.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@babel/code-frame": "^7.0.0",
+        "error-ex": "^1.3.1",
+        "json-parse-even-better-errors": "^2.3.0",
+        "lines-and-columns": "^1.1.6"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/path-exists": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/path-is-absolute": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "../node_modules/path-key": {
+      "version": "3.1.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/path-parse": {
+      "version": "1.0.7",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/path-scurry": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "../node_modules/path-type": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/picocolors": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/picomatch": {
+      "version": "2.3.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "../node_modules/possible-typed-array-names": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "../node_modules/prettier": {
+      "version": "3.5.3",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "prettier": "bin/prettier.cjs"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/prettier/prettier?sponsor=1"
+      }
+    },
+    "../node_modules/prettier-plugin-java": {
+      "version": "2.6.8",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "java-parser": "2.3.4",
+        "lodash": "4.17.21"
+      },
+      "peerDependencies": {
+        "prettier": "^3.0.0"
+      }
+    },
+    "../node_modules/punycode": {
+      "version": "2.3.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "../node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "../node_modules/reflect.getprototypeof": {
+      "version": "1.0.10",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.9",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0",
+        "get-intrinsic": "^1.2.7",
+        "get-proto": "^1.0.1",
+        "which-builtin-type": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/regexp.prototype.flags": {
+      "version": "1.5.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "define-properties": "^1.2.1",
+        "es-errors": "^1.3.0",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "set-function-name": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/resolve": {
+      "version": "1.22.10",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-core-module": "^2.16.0",
+        "path-parse": "^1.0.7",
+        "supports-preserve-symlinks-flag": "^1.0.0"
+      },
+      "bin": {
+        "resolve": "bin/resolve"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/resolve-from": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "../node_modules/reusify": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "../node_modules/rimraf": {
+      "version": "6.0.1",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^11.0.0",
+        "package-json-from-dist": "^1.0.0"
+      },
+      "bin": {
+        "rimraf": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "../node_modules/rollup": {
+      "version": "4.43.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.7"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.43.0",
+        "@rollup/rollup-android-arm64": "4.43.0",
+        "@rollup/rollup-darwin-arm64": "4.43.0",
+        "@rollup/rollup-darwin-x64": "4.43.0",
+        "@rollup/rollup-freebsd-arm64": "4.43.0",
+        "@rollup/rollup-freebsd-x64": "4.43.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.43.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.43.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.43.0",
+        "@rollup/rollup-linux-arm64-musl": "4.43.0",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.43.0",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.43.0",
+        "@rollup/rollup-linux-riscv64-musl": "4.43.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.43.0",
+        "@rollup/rollup-linux-x64-gnu": "4.43.0",
+        "@rollup/rollup-linux-x64-musl": "4.43.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.43.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.43.0",
+        "@rollup/rollup-win32-x64-msvc": "4.43.0",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "../node_modules/run-parallel": {
+      "version": "1.2.0",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "../node_modules/safe-array-concat": {
+      "version": "1.1.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "get-intrinsic": "^1.2.6",
+        "has-symbols": "^1.1.0",
+        "isarray": "^2.0.5"
+      },
+      "engines": {
+        "node": ">=0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/safe-push-apply": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "isarray": "^2.0.5"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/safe-regex-test": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "is-regex": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/semver": {
+      "version": "7.7.2",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "../node_modules/set-function-length": {
+      "version": "1.2.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2",
+        "get-intrinsic": "^1.2.4",
+        "gopd": "^1.0.1",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/set-function-name": {
+      "version": "2.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-data-property": "^1.1.4",
+        "es-errors": "^1.3.0",
+        "functions-have-names": "^1.2.3",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/set-proto": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/shebang-command": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/side-channel": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3",
+        "side-channel-list": "^1.0.0",
+        "side-channel-map": "^1.0.1",
+        "side-channel-weakmap": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/side-channel-list": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/side-channel-map": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/side-channel-weakmap": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.5",
+        "object-inspect": "^1.13.3",
+        "side-channel-map": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/signal-exit": {
+      "version": "4.1.0",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "../node_modules/slash": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/slice-ansi": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "astral-regex": "^2.0.0",
+        "is-fullwidth-code-point": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "../node_modules/stop-iteration-iterator": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "internal-slot": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/string-width": {
+      "version": "5.1.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/string-width-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/string-width/node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "../node_modules/string-width/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "../node_modules/string.prototype.trim": {
+      "version": "1.2.10",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "define-data-property": "^1.1.4",
+        "define-properties": "^1.2.1",
+        "es-abstract": "^1.23.5",
+        "es-object-atoms": "^1.0.0",
+        "has-property-descriptors": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/string.prototype.trimend": {
+      "version": "1.0.9",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.2",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/string.prototype.trimstart": {
+      "version": "1.0.8",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "define-properties": "^1.2.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/strip-bom": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "../node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/supports-color": {
+      "version": "7.2.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/supports-preserve-symlinks-flag": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/swiftlint": {
+      "version": "2.0.0",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-fs": "^3.1.7",
+        "@ionic/utils-subprocess": "^3.0.1",
+        "cosmiconfig": "^9.0.0"
+      },
+      "bin": {
+        "node-swiftlint": "bin.js"
+      }
+    },
+    "../node_modules/swiftlint/node_modules/cosmiconfig": {
+      "version": "9.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "env-paths": "^2.2.1",
+        "import-fresh": "^3.3.0",
+        "js-yaml": "^4.1.0",
+        "parse-json": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/d-fischer"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.9.5"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "../node_modules/swiftlint/node_modules/typescript": {
+      "version": "5.8.3",
+      "dev": true,
+      "license": "Apache-2.0",
+      "optional": true,
+      "peer": true,
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "../node_modules/text-table": {
+      "version": "0.2.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "../node_modules/tree-kill": {
+      "version": "1.2.2",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "tree-kill": "cli.js"
+      }
+    },
+    "../node_modules/tsconfig-paths": {
+      "version": "3.15.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/json5": "^0.0.29",
+        "json5": "^1.0.2",
+        "minimist": "^1.2.6",
+        "strip-bom": "^3.0.0"
+      }
+    },
+    "../node_modules/tslib": {
+      "version": "2.8.1",
+      "dev": true,
+      "license": "0BSD"
+    },
+    "../node_modules/tsutils": {
+      "version": "3.21.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^1.8.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      },
+      "peerDependencies": {
+        "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
+      }
+    },
+    "../node_modules/tsutils/node_modules/tslib": {
+      "version": "1.14.1",
+      "dev": true,
+      "license": "0BSD"
+    },
+    "../node_modules/type-check": {
+      "version": "0.4.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "../node_modules/type-fest": {
+      "version": "0.20.2",
+      "dev": true,
+      "license": "(MIT OR CC0-1.0)",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "../node_modules/typed-array-buffer": {
+      "version": "1.0.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "es-errors": "^1.3.0",
+        "is-typed-array": "^1.1.14"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "../node_modules/typed-array-byte-length": {
+      "version": "1.0.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.8",
+        "for-each": "^0.3.3",
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.14"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/typed-array-byte-offset": {
+      "version": "1.0.4",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "for-each": "^0.3.3",
+        "gopd": "^1.2.0",
+        "has-proto": "^1.2.0",
+        "is-typed-array": "^1.1.15",
+        "reflect.getprototypeof": "^1.0.9"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/typed-array-length": {
+      "version": "1.0.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bind": "^1.0.7",
+        "for-each": "^0.3.3",
+        "gopd": "^1.0.1",
+        "is-typed-array": "^1.1.13",
+        "possible-typed-array-names": "^1.0.0",
+        "reflect.getprototypeof": "^1.0.6"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/typescript": {
+      "version": "4.1.6",
+      "dev": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=4.2.0"
+      }
+    },
+    "../node_modules/unbox-primitive": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.3",
+        "has-bigints": "^1.0.2",
+        "has-symbols": "^1.1.0",
+        "which-boxed-primitive": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/universalify": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "../node_modules/untildify": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/uri-js": {
+      "version": "4.4.1",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "../node_modules/which": {
+      "version": "2.0.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "../node_modules/which-boxed-primitive": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-bigint": "^1.1.0",
+        "is-boolean-object": "^1.2.1",
+        "is-number-object": "^1.1.1",
+        "is-string": "^1.1.1",
+        "is-symbol": "^1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/which-builtin-type": {
+      "version": "1.2.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "call-bound": "^1.0.2",
+        "function.prototype.name": "^1.1.6",
+        "has-tostringtag": "^1.0.2",
+        "is-async-function": "^2.0.0",
+        "is-date-object": "^1.1.0",
+        "is-finalizationregistry": "^1.1.0",
+        "is-generator-function": "^1.0.10",
+        "is-regex": "^1.2.1",
+        "is-weakref": "^1.0.2",
+        "isarray": "^2.0.5",
+        "which-boxed-primitive": "^1.1.0",
+        "which-collection": "^1.0.2",
+        "which-typed-array": "^1.1.16"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/which-collection": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-map": "^2.0.3",
+        "is-set": "^2.0.3",
+        "is-weakmap": "^2.0.2",
+        "is-weakset": "^2.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/which-typed-array": {
+      "version": "1.1.19",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "available-typed-arrays": "^1.0.7",
+        "call-bind": "^1.0.8",
+        "call-bound": "^1.0.4",
+        "for-each": "^0.3.5",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-tostringtag": "^1.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "../node_modules/word-wrap": {
+      "version": "1.2.5",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "../node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "../node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "../node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "../node_modules/wrap-ansi-cjs/node_modules/string-width": {
+      "version": "4.2.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "../node_modules/wrap-ansi/node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "../node_modules/wrap-ansi/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "../node_modules/wrap-ansi/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "../node_modules/wrappy": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "ISC"
+    },
+    "../node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@capacitor/android": {
+      "version": "7.0.0",
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": "^7.0.0"
+      }
+    },
+    "node_modules/@capacitor/cli": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.3.0.tgz",
+      "integrity": "sha512-p0E1ayxw0Njpid8xwOrnuBncdakWxDMbUL2JhDUft38q8tscF2beIIMVhdna1t4Ow55H0r8sdTurwtSjtomrVw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/cli-framework-output": "^2.2.8",
+        "@ionic/utils-subprocess": "^3.0.1",
+        "@ionic/utils-terminal": "^2.3.5",
+        "commander": "^12.1.0",
+        "debug": "^4.4.0",
+        "env-paths": "^2.2.0",
+        "fs-extra": "^11.2.0",
+        "kleur": "^4.1.5",
+        "native-run": "^2.0.1",
+        "open": "^8.4.0",
+        "plist": "^3.1.0",
+        "prompts": "^2.4.2",
+        "rimraf": "^6.0.1",
+        "semver": "^7.6.3",
+        "tar": "^6.1.11",
+        "tslib": "^2.8.1",
+        "xml2js": "^0.6.2"
+      },
+      "bin": {
+        "cap": "bin/capacitor",
+        "capacitor": "bin/capacitor"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@capacitor/core": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-7.3.0.tgz",
+      "integrity": "sha512-t/DdTyBchQ2eAZuCmAARlqQsrEm0WyeNwh5zeRuv+cR6gnAsw+86/EWvJ/em5dTnZyaqEy8vlmOMdWarrUbnuQ==",
+      "license": "MIT",
+      "dependencies": {
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/@capacitor/ios": {
+      "version": "7.0.0",
+      "license": "MIT",
+      "peerDependencies": {
+        "@capacitor/core": "^7.0.0"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.21.5",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@ionic/cli-framework-output": {
+      "version": "2.2.8",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-terminal": "2.3.5",
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-array": {
+      "version": "2.1.6",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-fs": {
+      "version": "3.1.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/fs-extra": "^8.0.0",
+        "debug": "^4.0.0",
+        "fs-extra": "^9.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-fs/node_modules/fs-extra": {
+      "version": "9.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "at-least-node": "^1.0.0",
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/@ionic/utils-object": {
+      "version": "2.1.6",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-process": {
+      "version": "2.1.12",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-object": "2.1.6",
+        "@ionic/utils-terminal": "2.3.5",
+        "debug": "^4.0.0",
+        "signal-exit": "^3.0.3",
+        "tree-kill": "^1.2.2",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-stream": {
+      "version": "3.1.7",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-subprocess": {
+      "version": "3.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-array": "2.1.6",
+        "@ionic/utils-fs": "3.1.7",
+        "@ionic/utils-process": "2.1.12",
+        "@ionic/utils-stream": "3.1.7",
+        "@ionic/utils-terminal": "2.3.5",
+        "cross-spawn": "^7.0.3",
+        "debug": "^4.0.0",
+        "tslib": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@ionic/utils-terminal": {
+      "version": "2.3.5",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/slice-ansi": "^4.0.0",
+        "debug": "^4.0.0",
+        "signal-exit": "^3.0.3",
+        "slice-ansi": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0",
+        "tslib": "^2.0.1",
+        "untildify": "^4.0.0",
+        "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/@isaacs/balanced-match": {
+      "version": "4.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/@isaacs/brace-expansion": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@isaacs/balanced-match": "^4.0.1"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@isaacs/cliui/node_modules/string-width": {
+      "version": "5.1.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.43.0",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.7",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/fs-extra": {
+      "version": "8.1.5",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "24.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.8.0"
+      }
+    },
+    "node_modules/@types/slice-ansi": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@xmldom/xmldom": {
+      "version": "0.8.10",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      }
+    },
+    "node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/astral-regex": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/at-least-node": {
+      "version": "1.0.0",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">= 4.0.0"
+      }
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/big-integer": {
+      "version": "1.6.52",
+      "dev": true,
+      "license": "Unlicense",
+      "engines": {
+        "node": ">=0.6"
+      }
+    },
+    "node_modules/bplist-parser": {
+      "version": "0.3.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "big-integer": "1.6.x"
+      },
+      "engines": {
+        "node": ">= 5.10.0"
+      }
+    },
+    "node_modules/buffer-crc32": {
+      "version": "0.2.13",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/chownr": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/commander": {
+      "version": "12.1.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/define-lazy-prop": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dumon-geolocation": {
+      "resolved": "..",
+      "link": true
+    },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/elementtree": {
+      "version": "0.1.7",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "sax": "1.1.4"
+      },
+      "engines": {
+        "node": ">= 0.4.0"
+      }
+    },
+    "node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/env-paths": {
+      "version": "2.2.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/esbuild": {
+      "version": "0.21.5",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.21.5",
+        "@esbuild/android-arm": "0.21.5",
+        "@esbuild/android-arm64": "0.21.5",
+        "@esbuild/android-x64": "0.21.5",
+        "@esbuild/darwin-arm64": "0.21.5",
+        "@esbuild/darwin-x64": "0.21.5",
+        "@esbuild/freebsd-arm64": "0.21.5",
+        "@esbuild/freebsd-x64": "0.21.5",
+        "@esbuild/linux-arm": "0.21.5",
+        "@esbuild/linux-arm64": "0.21.5",
+        "@esbuild/linux-ia32": "0.21.5",
+        "@esbuild/linux-loong64": "0.21.5",
+        "@esbuild/linux-mips64el": "0.21.5",
+        "@esbuild/linux-ppc64": "0.21.5",
+        "@esbuild/linux-riscv64": "0.21.5",
+        "@esbuild/linux-s390x": "0.21.5",
+        "@esbuild/linux-x64": "0.21.5",
+        "@esbuild/netbsd-x64": "0.21.5",
+        "@esbuild/openbsd-x64": "0.21.5",
+        "@esbuild/sunos-x64": "0.21.5",
+        "@esbuild/win32-arm64": "0.21.5",
+        "@esbuild/win32-ia32": "0.21.5",
+        "@esbuild/win32-x64": "0.21.5"
+      }
+    },
+    "node_modules/fd-slicer": {
+      "version": "1.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pend": "~1.2.0"
+      }
+    },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/foreground-child/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/fs-extra": {
+      "version": "11.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^6.0.1",
+        "universalify": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
+    "node_modules/fs-minipass": {
+      "version": "2.1.0",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/fs-minipass/node_modules/minipass": {
+      "version": "3.3.6",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/glob": {
+      "version": "11.0.3",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.3.1",
+        "jackspeak": "^4.1.1",
+        "minimatch": "^10.0.3",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^2.0.0"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/graceful-fs": {
+      "version": "4.2.11",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/ini": {
+      "version": "4.1.3",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+      }
+    },
+    "node_modules/is-docker": {
+      "version": "2.2.1",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "is-docker": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/is-wsl": {
+      "version": "2.2.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-docker": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/jackspeak": {
+      "version": "4.1.1",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/jsonfile": {
+      "version": "6.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "universalify": "^2.0.0"
+      },
+      "optionalDependencies": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "node_modules/kleur": {
+      "version": "4.1.5",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/lru-cache": {
+      "version": "11.1.0",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": "20 || >=22"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "10.0.3",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "@isaacs/brace-expansion": "^5.0.0"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/minizlib": {
+      "version": "2.1.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minipass": "^3.0.0",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minizlib/node_modules/minipass": {
+      "version": "3.3.6",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/mkdirp": {
+      "version": "1.0.4",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/native-run": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@ionic/utils-fs": "^3.1.7",
+        "@ionic/utils-terminal": "^2.3.4",
+        "bplist-parser": "^0.3.2",
+        "debug": "^4.3.4",
+        "elementtree": "^0.1.7",
+        "ini": "^4.1.1",
+        "plist": "^3.1.0",
+        "split2": "^4.2.0",
+        "through2": "^4.0.2",
+        "tslib": "^2.6.2",
+        "yauzl": "^2.10.0"
+      },
+      "bin": {
+        "native-run": "bin/native-run"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/open": {
+      "version": "8.4.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "define-lazy-prop": "^2.0.0",
+        "is-docker": "^2.1.1",
+        "is-wsl": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-scurry": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^11.0.0",
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/pend": {
+      "version": "1.2.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/plist": {
+      "version": "3.1.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@xmldom/xmldom": "^0.8.8",
+        "base64-js": "^1.5.1",
+        "xmlbuilder": "^15.1.1"
+      },
+      "engines": {
+        "node": ">=10.4.0"
+      }
+    },
+    "node_modules/postcss": {
+      "version": "8.5.5",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/prompts": {
+      "version": "2.4.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "kleur": "^3.0.3",
+        "sisteransi": "^1.0.5"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/prompts/node_modules/kleur": {
+      "version": "3.0.3",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/rimraf": {
+      "version": "6.0.1",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^11.0.0",
+        "package-json-from-dist": "^1.0.0"
+      },
+      "bin": {
+        "rimraf": "dist/esm/bin.mjs"
+      },
+      "engines": {
+        "node": "20 || >=22"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/rollup": {
+      "version": "4.43.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.7"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.43.0",
+        "@rollup/rollup-android-arm64": "4.43.0",
+        "@rollup/rollup-darwin-arm64": "4.43.0",
+        "@rollup/rollup-darwin-x64": "4.43.0",
+        "@rollup/rollup-freebsd-arm64": "4.43.0",
+        "@rollup/rollup-freebsd-x64": "4.43.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.43.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.43.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.43.0",
+        "@rollup/rollup-linux-arm64-musl": "4.43.0",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.43.0",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.43.0",
+        "@rollup/rollup-linux-riscv64-musl": "4.43.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.43.0",
+        "@rollup/rollup-linux-x64-gnu": "4.43.0",
+        "@rollup/rollup-linux-x64-musl": "4.43.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.43.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.43.0",
+        "@rollup/rollup-win32-x64-msvc": "4.43.0",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/sax": {
+      "version": "1.1.4",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/semver": {
+      "version": "7.7.2",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/signal-exit": {
+      "version": "3.0.7",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/sisteransi": {
+      "version": "1.0.5",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/slice-ansi": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "astral-regex": "^2.0.0",
+        "is-fullwidth-code-point": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/split2": {
+      "version": "4.2.0",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/string-width": {
+      "version": "4.2.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tar": {
+      "version": "6.2.1",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.0.0",
+        "minipass": "^5.0.0",
+        "minizlib": "^2.1.1",
+        "mkdirp": "^1.0.3",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/tar/node_modules/minipass": {
+      "version": "5.0.0",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/through2": {
+      "version": "4.0.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "readable-stream": "3"
+      }
+    },
+    "node_modules/tree-kill": {
+      "version": "1.2.2",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "tree-kill": "cli.js"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "license": "0BSD"
+    },
+    "node_modules/undici-types": {
+      "version": "7.8.0",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/universalify": {
+      "version": "2.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/untildify": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/vite": {
+      "version": "5.4.19",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.21.3",
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^18.0.0 || >=20.0.0",
+        "less": "*",
+        "lightningcss": "^1.21.0",
+        "sass": "*",
+        "sass-embedded": "*",
+        "stylus": "*",
+        "sugarss": "*",
+        "terser": "^5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/wrap-ansi": {
+      "version": "7.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/xml2js": {
+      "version": "0.6.2",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "sax": ">=0.6.0",
+        "xmlbuilder": "~11.0.0"
+      },
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/xml2js/node_modules/xmlbuilder": {
+      "version": "11.0.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/xmlbuilder": {
+      "version": "15.1.1",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/yallist": {
+      "version": "4.0.0",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/yauzl": {
+      "version": "2.10.0",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-crc32": "~0.2.3",
+        "fd-slicer": "~1.1.0"
+      }
+    }
+  }
+}
diff --git a/example-app/package.json b/example-app/package.json
index bce3cbd..a69df9d 100644
--- a/example-app/package.json
+++ b/example-app/package.json
@@ -13,10 +13,10 @@
     "preview": "vite preview"
   },
   "dependencies": {
+    "@capacitor/android": "7.0.0",
     "@capacitor/core": "latest",
-    "dumon-geolocation": "file:..",
     "@capacitor/ios": "7.0.0",
-    "@capacitor/android": "7.0.0"
+    "dumon-geolocation": "file:.."
   },
   "devDependencies": {
     "@capacitor/cli": "latest",
@@ -24,4 +24,4 @@
   },
   "author": "",
   "license": "ISC"
-}
\ No newline at end of file
+}
diff --git a/example-app/src/index.html b/example-app/src/index.html
index ae96d8d..bf83e55 100644
--- a/example-app/src/index.html
+++ b/example-app/src/index.html
@@ -1,26 +1,58 @@
 
-
+
   
     
-    Example Capacitor App
+    Example Capacitor App - DumonGeolocation
     
     
+    
   
   
-    
-

Capacitor Test Plugin Project

-

- This project can be used to test out the functionality of your plugin. Nothing in the - example-app/ folder will be published to npm when using this template, so you can create away! -

- - - -
+

Capacitor DumonGeolocation Plugin Test

- + + + + +

+
+    
   
-
+
\ No newline at end of file
diff --git a/example-app/src/js/example.js b/example-app/src/js/example.js
index 6940a32..04191f6 100644
--- a/example-app/src/js/example.js
+++ b/example-app/src/js/example.js
@@ -1,6 +1,46 @@
 import { DumonGeolocation } from 'dumon-geolocation';
 
-window.testEcho = () => {
-    const inputValue = document.getElementById("echoInput").value;
-    DumonGeolocation.echo({ value: inputValue })
+const logArea = document.getElementById('logArea');
+
+function appendLog(title, data) {
+  const timestamp = new Date().toLocaleTimeString();
+  const formatted = `[${timestamp}] ${title}\n${JSON.stringify(data, null, 2)}\n\n`;
+  logArea.textContent = formatted; // + logArea.textContent;
 }
+
+async function startGeolocation() {
+  DumonGeolocation.addListener('onPositionUpdate', (data) => {
+    appendLog('onPositionUpdate', data);
+  });
+
+  try {
+    await DumonGeolocation.startPositioning();
+    appendLog('startPositioning', { success: true });
+  } catch (err) {
+    appendLog('startPositioning', { error: err.message });
+  }
+}
+
+async function stopGeolocation() {
+  try {
+    await DumonGeolocation.stopPositioning();
+    appendLog('stopPositioning', { success: true });
+  } catch (err) {
+    appendLog('stopPositioning', { error: err.message });
+  }
+}
+
+async function getLatestPosition() {
+  try {
+    const data = await DumonGeolocation.getLatestPosition();
+    appendLog('getLatestPosition', data);
+  } catch (err) {
+    appendLog('getLatestPosition', { error: err.message });
+  }
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+  document.getElementById('startButton').addEventListener('click', startGeolocation);
+  document.getElementById('stopButton').addEventListener('click', stopGeolocation);
+  document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+});
\ No newline at end of file
diff --git a/src/definitions.ts b/src/definitions.ts
index 9ae8e14..1c0c36c 100644
--- a/src/definitions.ts
+++ b/src/definitions.ts
@@ -1,3 +1,63 @@
-export interface DumonGeolocationPlugin {
-  echo(options: { value: string }): Promise<{ value: string }>;
+import type { PluginListenerHandle } from '@capacitor/core';
+
+export interface SatelliteStatus {
+  satellitesInView: number;
+  usedInFix: number;
+  constellationCounts: { [key: string]: number };
 }
+
+export interface WifiAp {
+  ssid: string;
+  bssid: string;
+  rssi: number;
+  distance?: number;
+}
+
+export interface WifiScanResult {
+  apCount: number;
+  aps: WifiAp[];
+}
+
+export interface ImuData {
+  accelX: number;
+  accelY: number;
+  accelZ: number;
+  gyroX: number;
+  gyroY: number;
+  gyroZ: number;
+  speed?: number;
+  acceleration?: number;
+  directionRad?: number;
+}
+
+export interface GpsData {
+  latitude: number;
+  longitude: number;
+  accuracy: number;
+  satellitesInView?: number;
+  usedInFix?: number;
+  constellationCounts?: { [key: string]: number };
+}
+
+export interface PositioningData {
+  source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';
+  timestamp: number;
+  latitude: number;
+  longitude: number;
+  accuracy: number;
+
+  gnssData?: SatelliteStatus;
+  wifiData?: WifiAp[];
+  imuData?: ImuData;
+}
+
+export interface DumonGeolocationPlugin {
+  startPositioning(): Promise;
+  stopPositioning(): Promise;
+  getLatestPosition(): Promise;
+
+  addListener(
+    eventName: 'onPositionUpdate',
+    listenerFunc: (data: PositioningData) => void
+  ): PluginListenerHandle;
+}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 4f8aba3..4367fe5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,4 +7,4 @@ const DumonGeolocation = registerPlugin('DumonGeolocatio
 });
 
 export * from './definitions';
-export { DumonGeolocation };
+export { DumonGeolocation };
\ No newline at end of file
diff --git a/src/web.ts b/src/web.ts
index f0e5f85..a5c2df4 100644
--- a/src/web.ts
+++ b/src/web.ts
@@ -1,10 +1,40 @@
 import { WebPlugin } from '@capacitor/core';
+import type { PositioningData } from './definitions';
 
-import type { DumonGeolocationPlugin } from './definitions';
-
-export class DumonGeolocationWeb extends WebPlugin implements DumonGeolocationPlugin {
-  async echo(options: { value: string }): Promise<{ value: string }> {
-    console.log('ECHO', options);
-    return options;
+export class DumonGeolocationWeb extends WebPlugin {
+  async startPositioning(): Promise {
+    console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
   }
-}
+
+  async stopPositioning(): Promise {
+    console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
+  }
+
+  async getLatestPosition(): Promise {
+    console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');
+    return {
+      source: 'GNSS',
+      timestamp: Date.now(),
+      latitude: 0,
+      longitude: 0,
+      accuracy: 999,
+      gnssData: {
+        satellitesInView: 0,
+        usedInFix: 0,
+        constellationCounts: {}
+      },
+      wifiData: [],
+      imuData: {
+        accelX: 0,
+        accelY: 0,
+        accelZ: 0,
+        gyroX: 0,
+        gyroY: 0,
+        gyroZ: 0,
+        speed: 0,
+        acceleration: 0,
+        directionRad: 0
+      }
+    };
+  }
+}
\ No newline at end of file