diff --git a/CHANGELOG.md b/CHANGELOG.md index c904c98..fc7b597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 1.0.3 +* Fix ios pod issue + +## 1.0.1 +* Fix ios pod issue + +## 1.0.0 +* Update the Android implementation + ## 0.8.0 * Add guard on web (Web always returns an empty stream) * Heading accounts for orientation on iOS diff --git a/README.md b/README.md index f197bab..b2c6286 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# flutter_compass +# flutter_compass_v2 -[![pub package](https://img.shields.io/pub/v/flutter_compass.svg)](https://pub.dartlang.org/packages/flutter_compass) +[![pub package](https://img.shields.io/pub/v/flutter_compass.svg)](https://pub.dartlang.org/packages/flutter_compass_v2) A Flutter compass. The heading varies from 0-360, 0 being north. @@ -14,7 +14,7 @@ To use this plugin, add `flutter_compass` as a [dependency in your pubspec.yaml ```yaml dependencies: - flutter_compass: '^0.7.0' + flutter_compass_v2: '^1.0.0' ``` ### iOS diff --git a/android/build.gradle b/android/build.gradle index 42093ef..8fa0466 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,27 +2,33 @@ group 'com.hemanthraj.fluttercompass' version '1.0-SNAPSHOT' buildscript { + ext { + kotlin_version = '1.9.22' + } repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.1' + classpath 'com.android.tools.build:gradle:7.4.2' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + namespace "com.hemanthraj.fluttercompass" + compileSdk 33 defaultConfig { minSdkVersion 20 @@ -31,4 +37,13 @@ android { lintOptions { disable 'InvalidPackage' } -} + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } +} \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 4034daa..a0419d7 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed Oct 14 23:53:48 IST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip \ No newline at end of file diff --git a/android/src/main/java/com/hemanthraj/fluttercompass/FlutterCompassPlugin.java b/android/src/main/java/com/hemanthraj/fluttercompass/FlutterCompassPlugin.java deleted file mode 100644 index 2011f29..0000000 --- a/android/src/main/java/com/hemanthraj/fluttercompass/FlutterCompassPlugin.java +++ /dev/null @@ -1,358 +0,0 @@ -package com.hemanthraj.fluttercompass; - -import android.content.Context; -import android.hardware.Sensor; -import android.hardware.SensorEvent; -import android.hardware.SensorEventListener; -import android.hardware.SensorManager; -import android.hardware.display.DisplayManager; -import android.os.SystemClock; -import android.util.Log; -import android.view.Surface; -import android.view.Display; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.EventChannel.EventSink; -import io.flutter.plugin.common.EventChannel.StreamHandler; - -public final class FlutterCompassPlugin implements FlutterPlugin, StreamHandler { - private static final String TAG = "FlutterCompass"; - // The rate sensor events will be delivered at. As the Android documentation - // states, this is only a hint to the system and the events might actually be - // received faster or slower than this specified rate. Since the minimum - // Android API levels about 9, we are able to set this value ourselves rather - // than using one of the provided constants which deliver updates too quickly - // for our use case. The default is set to 100ms - private static final int SENSOR_DELAY_MICROS = 30 * 1000; - - // Filtering coefficient 0 < ALPHA < 1 - private static final float ALPHA = 0.45f; - - // Controls the compass update rate in milliseconds - private static final int COMPASS_UPDATE_RATE_MS = 32; - - private SensorEventListener sensorEventListener; - - private Display display; - @Nullable - private SensorManager sensorManager; - - @Nullable - private Sensor compassSensor; - @Nullable - private Sensor gravitySensor; - @Nullable - private Sensor magneticFieldSensor; - - private float[] truncatedRotationVectorValue = new float[4]; - private float[] rotationMatrix = new float[9]; - private float[] rotationVectorValue; - private float lastHeading; - private int lastAccuracySensorStatus; - - private long compassUpdateNextTimestamp; - private float[] gravityValues = new float[3]; - private float[] magneticValues = new float[3]; - - @Nullable - private EventChannel channel; - - public FlutterCompassPlugin() { - // no-op - } - - private void getSensors(Context context) { - display = ((DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE)) - .getDisplay(Display.DEFAULT_DISPLAY); - sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); - compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); - if (compassSensor == null) { - Log.d(TAG, "Rotation vector sensor not supported on device, " - + "falling back to accelerometer and magnetic field."); - } - - gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); - magneticFieldSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); - } - - private void cleanSensors() { - sensorManager = null; - display = null; - compassSensor = null; - gravitySensor = null; - magneticFieldSensor = null; - } - - // New Plugin APIs - - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { - channel = new EventChannel(binding.getBinaryMessenger(), "hemanthraj/flutter_compass"); - getSensors(binding.getApplicationContext()); - channel.setStreamHandler(this); - } - - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { - unregisterListener(); - cleanSensors(); - if (channel != null) channel.setStreamHandler(null); - } - - public void onListen(Object arguments, EventSink events) { - sensorEventListener = createSensorEventListener(events); - registerListener(); - } - - public void onCancel(Object arguments) { - unregisterListener(); - } - - private void registerListener() { - if (sensorManager == null) return; - if (isCompassSensorAvailable()) { - // Does nothing if the sensors already registered. - sensorManager.registerListener(sensorEventListener, compassSensor, SENSOR_DELAY_MICROS); - } - - sensorManager.registerListener(sensorEventListener, gravitySensor, SENSOR_DELAY_MICROS); - sensorManager.registerListener(sensorEventListener, magneticFieldSensor, SENSOR_DELAY_MICROS); - } - - private void unregisterListener() { - if (sensorManager == null) return; - if (isCompassSensorAvailable()) { - sensorManager.unregisterListener(sensorEventListener, compassSensor); - } - - sensorManager.unregisterListener(sensorEventListener, gravitySensor); - sensorManager.unregisterListener(sensorEventListener, magneticFieldSensor); - } - - private boolean isCompassSensorAvailable() { - return compassSensor != null; - } - - SensorEventListener createSensorEventListener(final EventSink events) { - return new SensorEventListener() { - @Override - public void onSensorChanged(SensorEvent event) { - if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_UNRELIABLE) { - Log.d(TAG, "Compass sensor is unreliable, device calibration is needed."); - // Update the heading, even if the sensor is unreliable. - // This makes it possible to use a different indicator for the unreliable case, - // instead of just changing the RenderMode to NORMAL. - } - if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) { - rotationVectorValue = getRotationVectorFromSensorEvent(event); - updateOrientation(); - } else if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER && !isCompassSensorAvailable()) { - gravityValues = lowPassFilter(getRotationVectorFromSensorEvent(event), gravityValues); - updateOrientation(); - } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD && !isCompassSensorAvailable()) { - magneticValues = lowPassFilter(getRotationVectorFromSensorEvent(event), magneticValues); - updateOrientation(); - } - } - - @Override - public void onAccuracyChanged(Sensor sensor, int accuracy) { - if (lastAccuracySensorStatus != accuracy) { - lastAccuracySensorStatus = accuracy; - } - } - - @SuppressWarnings("SuspiciousNameCombination") - private void updateOrientation() { - // check when the last time the compass was updated, return if too soon. - long currentTime = SystemClock.elapsedRealtime(); - if (currentTime < compassUpdateNextTimestamp) { - return; - } - - if (rotationVectorValue != null) { - SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVectorValue); - } else { - // Get rotation matrix given the gravity and geomagnetic matrices - SensorManager.getRotationMatrix(rotationMatrix, null, gravityValues, magneticValues); - } - - int worldAxisForDeviceAxisX; - int worldAxisForDeviceAxisY; - - // Assume the device screen was parallel to the ground, - // and adjust the rotation matrix for the device orientation. - switch (display.getRotation()) { - case Surface.ROTATION_90: - worldAxisForDeviceAxisX = SensorManager.AXIS_Y; - worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X; - break; - case Surface.ROTATION_180: - worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X; - worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Y; - break; - case Surface.ROTATION_270: - worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Y; - worldAxisForDeviceAxisY = SensorManager.AXIS_X; - break; - case Surface.ROTATION_0: - default: - worldAxisForDeviceAxisX = SensorManager.AXIS_X; - worldAxisForDeviceAxisY = SensorManager.AXIS_Y; - break; - } - - float[] adjustedRotationMatrix = new float[9]; - SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisX, worldAxisForDeviceAxisY, - adjustedRotationMatrix); - - // Transform rotation matrix into azimuth/pitch/roll - float[] orientation = new float[3]; - SensorManager.getOrientation(adjustedRotationMatrix, orientation); - - if (orientation[1] < -Math.PI / 4) { - // The pitch is less than -45 degrees. - // Remap the axes as if the device screen was the instrument panel. - switch (display.getRotation()) { - case Surface.ROTATION_90: - worldAxisForDeviceAxisX = SensorManager.AXIS_Z; - worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X; - break; - case Surface.ROTATION_180: - worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X; - worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Z; - break; - case Surface.ROTATION_270: - worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Z; - worldAxisForDeviceAxisY = SensorManager.AXIS_X; - break; - case Surface.ROTATION_0: - default: - worldAxisForDeviceAxisX = SensorManager.AXIS_X; - worldAxisForDeviceAxisY = SensorManager.AXIS_Z; - break; - } - } else if (orientation[1] > Math.PI / 4) { - // The pitch is larger than 45 degrees. - // Remap the axes as if the device screen was upside down and facing back. - switch (display.getRotation()) { - case Surface.ROTATION_90: - worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Z; - worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X; - break; - case Surface.ROTATION_180: - worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X; - worldAxisForDeviceAxisY = SensorManager.AXIS_Z; - break; - case Surface.ROTATION_270: - worldAxisForDeviceAxisX = SensorManager.AXIS_Z; - worldAxisForDeviceAxisY = SensorManager.AXIS_X; - break; - case Surface.ROTATION_0: - default: - worldAxisForDeviceAxisX = SensorManager.AXIS_X; - worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Z; - break; - } - } else if (Math.abs(orientation[2]) > Math.PI / 2) { - // The roll is less than -90 degrees, or is larger than 90 degrees. - // Remap the axes as if the device screen was face down. - switch (display.getRotation()) { - case Surface.ROTATION_90: - worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Y; - worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X; - break; - case Surface.ROTATION_180: - worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X; - worldAxisForDeviceAxisY = SensorManager.AXIS_Y; - break; - case Surface.ROTATION_270: - worldAxisForDeviceAxisX = SensorManager.AXIS_Y; - worldAxisForDeviceAxisY = SensorManager.AXIS_X; - break; - case Surface.ROTATION_0: - default: - worldAxisForDeviceAxisX = SensorManager.AXIS_X; - worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Y; - break; - } - } - - SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisX, worldAxisForDeviceAxisY, - adjustedRotationMatrix); - - // Transform rotation matrix into azimuth/pitch/roll - SensorManager.getOrientation(adjustedRotationMatrix, orientation); - - double[] v = new double[3]; - v[0] = Math.toDegrees(orientation[0]); - v[2] = getAccuracy(); - // The x-axis is all we care about here. - notifyCompassChangeListeners(v); - - // Update the compassUpdateNextTimestamp - compassUpdateNextTimestamp = currentTime + COMPASS_UPDATE_RATE_MS; - } - - private void notifyCompassChangeListeners(double[] heading) { - events.success(heading); - lastHeading = (float) heading[0]; - } - - private double getAccuracy() { - if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_ACCURACY_HIGH) { - return 15; - } else if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM) { - return 30; - } else if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_ACCURACY_LOW) { - return 45; - } else { - return -1; // unknown - } - } - - /** - * Helper function, that filters newValues, considering previous values - * - * @param newValues array of float, that contains new data - * @param smoothedValues array of float, that contains previous state - * @return float filtered array of float - */ - private float[] lowPassFilter(float[] newValues, float[] smoothedValues) { - if (smoothedValues == null) { - return newValues; - } - for (int i = 0; i < newValues.length; i++) { - smoothedValues[i] = smoothedValues[i] + ALPHA * (newValues[i] - smoothedValues[i]); - } - return smoothedValues; - } - - /** - * Pulls out the rotation vector from a SensorEvent, with a maximum length - * vector of four elements to avoid potential compatibility issues. - * - * @param event the sensor event - * @return the events rotation vector, potentially truncated - */ - @NonNull - private float[] getRotationVectorFromSensorEvent(@NonNull SensorEvent event) { - if (event.values.length > 4) { - // On some Samsung devices SensorManager.getRotationMatrixFromVector - // appears to throw an exception if rotation vector has length > 4. - // For the purposes of this class the first 4 values of the - // rotation vector are sufficient (see crbug.com/335298 for details). - // Only affects Android 4.3 - System.arraycopy(event.values, 0, truncatedRotationVectorValue, 0, 4); - return truncatedRotationVectorValue; - } else { - return event.values; - } - } - }; - } -} diff --git a/android/src/main/kotlin/com/hemanthraj/fluttercompass/FlutterCompassPlugin.kt b/android/src/main/kotlin/com/hemanthraj/fluttercompass/FlutterCompassPlugin.kt new file mode 100644 index 0000000..1ac692e --- /dev/null +++ b/android/src/main/kotlin/com/hemanthraj/fluttercompass/FlutterCompassPlugin.kt @@ -0,0 +1,246 @@ +package com.hemanthraj.fluttercompass + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.hardware.display.DisplayManager +import android.os.SystemClock +import android.util.Log +import android.view.Display +import android.view.Surface +import com.hemanthraj.fluttercompass.model.DisplayRotation +import com.hemanthraj.fluttercompass.model.RotationVector +import com.hemanthraj.fluttercompass.util.CompassHelper +import com.hemanthraj.fluttercompass.util.CompassHelper.calculateHeading +import com.hemanthraj.fluttercompass.util.CompassHelper.convertRadtoDeg +import com.hemanthraj.fluttercompass.util.CompassHelper.map180to360 +import com.hemanthraj.fluttercompass.util.MathUtils +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.EventChannel.EventSink +import kotlin.math.abs + + +class FlutterCompassPlugin : FlutterPlugin, EventChannel.StreamHandler { + private var compassSensorEventListener: SensorEventListener? = null + private var display: Display? = null + private var sensorManager: SensorManager? = null + private var rotationSensor: Sensor? = null + private var accelerometerSensor: Sensor? = null + private var magneticFieldSensor: Sensor? = null + private val truncatedRotationVectorValue = FloatArray(4) + private val rotationMatrix = FloatArray(9) + private var lastHeading = 0f + private var lastAccuracySensorStatus = 0 + private var compassUpdateNextTimestamp: Long = 0 + private var accelerometerReading = FloatArray(3) + private var magneticReading = FloatArray(3) + private var channel: EventChannel? = null + + private val isCompassSensorAvailable: Boolean + get() = rotationSensor != null + + // New Plugin APIs + override fun onAttachedToEngine(binding: FlutterPluginBinding) { + channel = EventChannel(binding.binaryMessenger, EVENT_NAME) + getSensors(binding.applicationContext) + channel?.setStreamHandler(this) + } + + override fun onDetachedFromEngine(binding: FlutterPluginBinding) { + unregisterListener() + cleanSensors() + channel?.setStreamHandler(null) + } + + + override fun onListen(arguments: Any?, events: EventSink?) { + events?.let { + compassSensorEventListener = CompassSensorEventListener(events) + registerListener() + } + } + + override fun onCancel(arguments: Any?) { + unregisterListener() + compassSensorEventListener = null + } + + private fun getSensors(context: Context) { + display = (context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager) + .getDisplay(Display.DEFAULT_DISPLAY) + sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + rotationSensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + if (rotationSensor == null) { + Log.d(TAG, "Rotation vector sensor not supported on device, " + + "falling back to accelerometer and magnetic field.") + accelerometerSensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + } + magneticFieldSensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD) + } + + private fun cleanSensors() { + sensorManager = null + display = null + rotationSensor = null + accelerometerSensor = null + magneticFieldSensor = null + } + + private fun registerListener() { + if (sensorManager == null) return + // Does nothing if the sensors already registered. + sensorManager!!.registerListener(compassSensorEventListener, magneticFieldSensor, SENSOR_DELAY_MICROS) + if (isCompassSensorAvailable) { + sensorManager!!.registerListener(compassSensorEventListener, rotationSensor, SENSOR_DELAY_MICROS) + } else { + sensorManager!!.registerListener(compassSensorEventListener, accelerometerSensor, SENSOR_DELAY_MICROS) + } + } + + private fun unregisterListener() { + if (sensorManager == null) return + sensorManager!!.unregisterListener(compassSensorEventListener, magneticFieldSensor) + if (isCompassSensorAvailable) { + sensorManager!!.unregisterListener(compassSensorEventListener, rotationSensor) + } else { + sensorManager!!.unregisterListener(compassSensorEventListener, accelerometerSensor) + } + } + + private inner class CompassSensorEventListener(val eventSink: EventSink) : SensorEventListener { + override fun onSensorChanged(event: SensorEvent) { + if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_UNRELIABLE) { + Log.d(TAG, "Compass sensor is unreliable, device calibration is needed.") + // Update the heading, even if the sensor is unreliable. + // This makes it possible to use a different indicator for the unreliable case, + // instead of just changing the RenderMode to NORMAL. + } + when { + event.sensor.type == Sensor.TYPE_ROTATION_VECTOR -> { + val rotationVectorReading = getRotationVectorFromSensorEvent(event) + updateRotationCompass(rotationVectorReading) + return + } + + event.sensor.type == Sensor.TYPE_ACCELEROMETER && !isCompassSensorAvailable -> { + CompassHelper.lowPassFilter(event.values.clone(), accelerometerReading) + updateHeading() + } + + event.sensor.type == Sensor.TYPE_MAGNETIC_FIELD && !isCompassSensorAvailable -> { + CompassHelper.lowPassFilter(event.values.clone(), magneticReading) + updateHeading() + } + } + } + + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { + when (sensor.type) { + Sensor.TYPE_MAGNETIC_FIELD -> if (lastAccuracySensorStatus != accuracy) { + lastAccuracySensorStatus = accuracy + } + + Sensor.TYPE_ROTATION_VECTOR -> Log.v(TAG, "Received rotation vector sensor accuracy $accuracy") + else -> Log.w(TAG, "Unexpected accuracy changed event of type ${sensor.type}") + } + + } + + private fun updateHeading() { + var heading = calculateHeading(accelerometerReading, magneticReading) + heading = convertRadtoDeg(heading) + heading = map180to360(heading) + + notifyCompassChangeListeners(heading.toDouble()) + } + + + private fun updateRotationCompass(rotationVectorValue: FloatArray) { + val rotationVector = RotationVector(rotationVectorValue[0], rotationVectorValue[1], rotationVectorValue[2]) + val displayRotation = getDisplayRotation() + val magneticAzimuth = MathUtils.calculateAzimuth(rotationVector, displayRotation) + + notifyCompassChangeListeners(magneticAzimuth.degrees.toDouble()) + } + + private fun getDisplayRotation(): DisplayRotation { + return when (display!!.rotation) { + Surface.ROTATION_90 -> DisplayRotation.ROTATION_90 + Surface.ROTATION_180 -> DisplayRotation.ROTATION_180 + Surface.ROTATION_270 -> DisplayRotation.ROTATION_270 + else -> DisplayRotation.ROTATION_0 + } + } + + private fun notifyCompassChangeListeners(degrees: Double) { + if(degrees.isNaN()) { + return + } + + val data = DoubleArray(3) + data[0] = degrees + data[1] = 0.0 + data[2] = accuracy.toDouble() + + eventSink.success(data) + lastHeading = degrees.toFloat() + } + + private val accuracy: Int + get() = when (lastAccuracySensorStatus) { + SensorManager.SENSOR_STATUS_ACCURACY_HIGH -> { + 15 + } + + SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM -> { + 30 + } + + SensorManager.SENSOR_STATUS_ACCURACY_LOW -> { + 45 + } + + else -> { + -1 + } + } + + /** + * Pulls out the rotation vector from a SensorEvent, with a maximum length + * vector of four elements to avoid potential compatibility issues. + * + * @param event the sensor event + * @return the events rotation vector, potentially truncated + */ + private fun getRotationVectorFromSensorEvent(event: SensorEvent): FloatArray { + return if (event.values.size > 4) { + // On some Samsung devices SensorManager.getRotationMatrixFromVector + // appears to throw an exception if rotation vector has length > 4. + // For the purposes of this class the first 4 values of the + // rotation vector are sufficient (see crbug.com/335298 for details). + // Only affects Android 4.3 + System.arraycopy(event.values, 0, truncatedRotationVectorValue, 0, 4) + truncatedRotationVectorValue + } else { + event.values + } + } + } + + companion object { + private const val TAG = "FlutterCompass" + private const val EVENT_NAME = "hemanthraj/flutter_compass" + + // The rate sensor events will be delivered at. As the Android documentation + // states, this is only a hint to the system and the events might actually be + // received faster or slower than this specified rate. Since the minimum + // Android API levels about 9, we are able to set this value ourselves rather + // than using one of the provided constants which deliver updates too quickly + // for our use case. The default is set to 100ms + private const val SENSOR_DELAY_MICROS = 30 * 1000 + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/Azimuth.kt b/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/Azimuth.kt new file mode 100644 index 0000000..d8e7c9e --- /dev/null +++ b/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/Azimuth.kt @@ -0,0 +1,77 @@ +/* + * This file is part of Compass. + * Copyright (C) 2023 Philipp Bobek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Compass is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.hemanthraj.fluttercompass.model + +import kotlin.math.roundToInt + +class Azimuth(_degrees: Float) { + + init { + if (!_degrees.isFinite()) { + throw IllegalArgumentException("Degrees must be finite but was '$_degrees'") + } + } + + val degrees = normalizeAngle(_degrees) + + val roundedDegrees = normalizeAngle(_degrees.roundToInt().toFloat()).toInt() + + val cardinalDirection: CardinalDirection = when (degrees) { + in 22.5f until 67.5f -> CardinalDirection.NORTHEAST + in 67.5f until 112.5f -> CardinalDirection.EAST + in 112.5f until 157.5f -> CardinalDirection.SOUTHEAST + in 157.5f until 202.5f -> CardinalDirection.SOUTH + in 202.5f until 247.5f -> CardinalDirection.SOUTHWEST + in 247.5f until 292.5f -> CardinalDirection.WEST + in 292.5f until 337.5f -> CardinalDirection.NORTHWEST + else -> CardinalDirection.NORTH + } + + private fun normalizeAngle(angleInDegrees: Float): Float { + return (angleInDegrees + 360f) % 360f + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Azimuth + + return degrees == other.degrees + } + + override fun hashCode(): Int { + return degrees.hashCode() + } + + override fun toString(): String { + return "Azimuth(degrees=$degrees)" + } + + operator fun plus(degrees: Float) = Azimuth(this.degrees + degrees) + + operator fun minus(degrees: Float) = Azimuth(this.degrees - degrees) + + operator fun compareTo(azimuth: Azimuth) = this.degrees.compareTo(azimuth.degrees) +} + +private data class SemiClosedFloatRange(val fromInclusive: Float, val toExclusive: Float) + +private operator fun SemiClosedFloatRange.contains(value: Float) = fromInclusive <= value && value < toExclusive +private infix fun Float.until(to: Float) = SemiClosedFloatRange(this, to) diff --git a/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/CardinalDirection.kt b/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/CardinalDirection.kt new file mode 100644 index 0000000..eb519f5 --- /dev/null +++ b/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/CardinalDirection.kt @@ -0,0 +1,31 @@ +/* + * This file is part of Compass. + * Copyright (C) 2021 Philipp Bobek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Compass is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.hemanthraj.fluttercompass.model + + +enum class CardinalDirection { + NORTH, + NORTHEAST, + EAST, + SOUTHEAST, + SOUTH, + SOUTHWEST, + WEST, + NORTHWEST +} diff --git a/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/DisplayRotation.kt b/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/DisplayRotation.kt new file mode 100644 index 0000000..f13028a --- /dev/null +++ b/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/DisplayRotation.kt @@ -0,0 +1,26 @@ +/* + * This file is part of Compass. + * Copyright (C) 2022 Philipp Bobek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Compass is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.hemanthraj.fluttercompass.model + +enum class DisplayRotation { + ROTATION_0, + ROTATION_90, + ROTATION_180, + ROTATION_270 +} diff --git a/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/RotationVector.kt b/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/RotationVector.kt new file mode 100644 index 0000000..927d1f9 --- /dev/null +++ b/android/src/main/kotlin/com/hemanthraj/fluttercompass/model/RotationVector.kt @@ -0,0 +1,24 @@ +/* + * This file is part of Compass. + * Copyright (C) 2021 Philipp Bobek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Compass is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.hemanthraj.fluttercompass.model + +data class RotationVector(val x: Float, val y: Float, val z: Float) { + + fun toArray(): FloatArray = floatArrayOf(x, y, z) +} diff --git a/android/src/main/kotlin/com/hemanthraj/fluttercompass/util/CompassHelper.kt b/android/src/main/kotlin/com/hemanthraj/fluttercompass/util/CompassHelper.kt new file mode 100644 index 0000000..78ab2fe --- /dev/null +++ b/android/src/main/kotlin/com/hemanthraj/fluttercompass/util/CompassHelper.kt @@ -0,0 +1,67 @@ +package com.hemanthraj.fluttercompass.util + +import java.util.GregorianCalendar +import kotlin.math.atan2 +import kotlin.math.sqrt + +object CompassHelper { + //0 ≤ ALPHA ≤ 1 + //smaller ALPHA results in smoother sensor data but slower updates + private const val ALPHA = 0.15f + fun calculateHeading(accelerometerReading: FloatArray, magnetometerReading: FloatArray): Float { + var Ax = accelerometerReading[0] + var Ay = accelerometerReading[1] + var Az = accelerometerReading[2] + val Ex = magnetometerReading[0] + val Ey = magnetometerReading[1] + val Ez = magnetometerReading[2] + + //cross product of the magnetic field vector and the gravity vector + var Hx = Ey * Az - Ez * Ay + var Hy = Ez * Ax - Ex * Az + var Hz = Ex * Ay - Ey * Ax + + //normalize the values of resulting vector + val invH = 1.0f / sqrt((Hx * Hx + Hy * Hy + Hz * Hz).toDouble()).toFloat() + Hx *= invH + Hy *= invH + Hz *= invH + + //normalize the values of gravity vector + val invA = 1.0f / sqrt((Ax * Ax + Ay * Ay + Az * Az).toDouble()).toFloat() + Ax *= invA + Ay *= invA + Az *= invA + + //cross product of the gravity vector and the new vector H + val Mx = Ay * Hz - Az * Hy + val My = Az * Hx - Ax * Hz + val Mz = Ax * Hy - Ay * Hx + + //arctangent to obtain heading in radians + return atan2(Hy.toDouble(), My.toDouble()).toFloat() + } + + fun calculateMagneticDeclination(latitude: Double, longitude: Double, altitude: Double): Float { + val geoMag = TSAGeoMag() + return geoMag + .getDeclination(latitude, longitude, geoMag.decimalYear(GregorianCalendar()), altitude).toFloat() + } + + fun convertRadtoDeg(rad: Float): Float { + return (rad / Math.PI).toFloat() * 180 + } + + //map angle from [-180,180] range to [0,360] range + fun map180to360(angle: Float): Float { + return (angle + 360) % 360 + } + + fun lowPassFilter(input: FloatArray, output: FloatArray?): FloatArray { + if (output == null) return input + for (i in input.indices) { + output[i] = output[i] + ALPHA * (input[i] - output[i]) + } + return output + } +} diff --git a/android/src/main/kotlin/com/hemanthraj/fluttercompass/util/MathUtils.kt b/android/src/main/kotlin/com/hemanthraj/fluttercompass/util/MathUtils.kt new file mode 100644 index 0000000..a429635 --- /dev/null +++ b/android/src/main/kotlin/com/hemanthraj/fluttercompass/util/MathUtils.kt @@ -0,0 +1,87 @@ +/* + * This file is part of Compass. + * Copyright (C) 2023 Philipp Bobek + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Compass is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.hemanthraj.fluttercompass.util + +import android.hardware.GeomagneticField +import android.hardware.SensorManager +import android.location.Location +import com.hemanthraj.fluttercompass.model.Azimuth +import com.hemanthraj.fluttercompass.model.DisplayRotation +import com.hemanthraj.fluttercompass.model.RotationVector +import kotlin.math.roundToInt + +private const val AZIMUTH = 0 +private const val AXIS_SIZE = 3 +private const val ROTATION_MATRIX_SIZE = 9 + +object MathUtils { + + @JvmStatic + fun calculateAzimuth(rotationVector: RotationVector, displayRotation: DisplayRotation): Azimuth { + val rotationMatrix = getRotationMatrix(rotationVector) + val remappedRotationMatrix = remapRotationMatrix(rotationMatrix, displayRotation) + val orientationInRadians = SensorManager.getOrientation(remappedRotationMatrix, FloatArray(AXIS_SIZE)) + val azimuthInRadians = orientationInRadians[AZIMUTH] + val azimuthInDegrees = Math.toDegrees(azimuthInRadians.toDouble()).toFloat() + return Azimuth(azimuthInDegrees) + } + + private fun getRotationMatrix(rotationVector: RotationVector): FloatArray { + val rotationMatrix = FloatArray(ROTATION_MATRIX_SIZE) + SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector.toArray()) + return rotationMatrix + } + + private fun remapRotationMatrix(rotationMatrix: FloatArray, displayRotation: DisplayRotation): FloatArray { + return when (displayRotation) { + DisplayRotation.ROTATION_0 -> remapRotationMatrix(rotationMatrix, SensorManager.AXIS_X, SensorManager.AXIS_Y) + DisplayRotation.ROTATION_90 -> remapRotationMatrix(rotationMatrix, SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X) + DisplayRotation.ROTATION_180 -> remapRotationMatrix(rotationMatrix, SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y) + DisplayRotation.ROTATION_270 -> remapRotationMatrix(rotationMatrix, SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_X) + } + } + + private fun remapRotationMatrix(rotationMatrix: FloatArray, newX: Int, newY: Int): FloatArray { + val remappedRotationMatrix = FloatArray(ROTATION_MATRIX_SIZE) + SensorManager.remapCoordinateSystem(rotationMatrix, newX, newY, remappedRotationMatrix) + return remappedRotationMatrix + } + + @JvmStatic + fun getMagneticDeclination(location: Location): Float { + val latitude = location.latitude.toFloat() + val longitude = location.longitude.toFloat() + val altitude = location.altitude.toFloat() + val time = location.time + val geomagneticField = GeomagneticField(latitude, longitude, altitude, time) + return geomagneticField.declination + } + + fun getClosestNumberFromInterval(number: Float, interval: Float): Float = + (number / interval).roundToInt() * interval + + /** + * @see Stackexchange + */ + fun isAzimuthBetweenTwoPoints(azimuth: Azimuth, pointA: Azimuth, pointB: Azimuth): Boolean { + val aToB = (pointB.degrees - pointA.degrees + 360f) % 360f + val aToAzimuth = (azimuth.degrees - pointA.degrees + 360f) % 360f + return aToB <= 180f != aToAzimuth > aToB + } +} diff --git a/android/src/main/kotlin/com/hemanthraj/fluttercompass/util/TSAGeoMag.java b/android/src/main/kotlin/com/hemanthraj/fluttercompass/util/TSAGeoMag.java new file mode 100644 index 0000000..2ededa0 --- /dev/null +++ b/android/src/main/kotlin/com/hemanthraj/fluttercompass/util/TSAGeoMag.java @@ -0,0 +1,1168 @@ +package com.hemanthraj.fluttercompass.util; +/* PUBLIC DOMAIN NOTICE +This program was prepared by Los Alamos National Security, LLC +at Los Alamos National Laboratory (LANL) under contract No. +DE-AC52-06NA25396 with the U.S. Department of Energy (DOE). +All rights in the program are reserved by the DOE and +Los Alamos National Security, LLC. Permission is granted to the +public to copy and use this software without charge, +provided that this Notice and any statement of authorship are +reproduced on all copies. Neither the U.S. Government nor LANS +makes any warranty, express or implied, or assumes any liability +or responsibility for the use of this software. + */ + +/* License Statement from the NOAA +The WMM source code is in the public domain and not licensed or +under copyright. The information and software may be used freely +by the public. As required by 17 U.S.C. 403, third parties producing +copyrighted works consisting predominantly of the material produced +by U.S. government agencies must provide notice with such work(s) +identifying the U.S. Government material incorporated and stating +that such material is not subject to copyright protection. + */ + +//////////////////////////////////////////////////////////////////////////// +// +//GeoMag.java - originally geomag.c +//Ported to Java 1.0.2 by Tim Walker +//tim.walker@worldnet.att.net +//tim@acusat.com +// +//Updated: 1/28/98 +// +//Original source geomag.c available at +//http://www.ngdc.noaa.gov/seg/potfld/DoDWMM.html +// +//NOTE: original comments from the geomag.c source file are in ALL CAPS +//Tim's added comments for the port to Java are not +// +//////////////////////////////////////////////////////////////////////////// + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StreamTokenizer; +import java.util.Calendar; +import java.util.GregorianCalendar; + +//import org.apache.log4j.Logger; + +/** + *

+ * Last updated on Jan 6, 2020

+ * NOTE: Comment out the logger references, and put back in the System.out.println + * statements if not using log4j in your application. Checks are not made on the method inputs + * to ensure they are within a valid range.

+ *

+ * Verified by a JUnit test using the test values distributed with the 2020 epoch update.

+ *

+ * This is a class to generate the magnetic declination, + * magnetic field strength and inclination for any point + * on the earth. The true bearing = magnetic bearing + declination. + * This class is adapted from an Applet from the NOAA National Data Center + * at http://www.ngdc.noaa.gov/seg/segd.shtml. + * None of the calculations + * were changed. This class requires an input file named WMM.COF, which + * must be in the same directory that the application is run from.
+ * NOTE: If the WMM.COF file is missing, the internal fit coefficients + * for 2020 will be used. + *

+ * Using the correct date, the declination is accurate to about 0.5 degrees.

+ *

+ * This is the LANL D-3 version of the GeoMagnetic calculator from + * the NOAA National Data Center at http://www.ngdc.noaa.gov/seg/segd.shtml.

+ *

+ * Adapted by John St. Ledger, Los Alamos National Laboratory + * June 25, 1999

+ *

+ *

+ * Version 2 Comments: The world magnetic model is updated every 5 years. + * The data for 2000 uses the same algorithm to calculate the magnetic + * field variables. The only change is in the spherical harmonic coefficients + * in the input file. The input file has been renamed to WMM.COF. Once again, + * the date was fixed. This time to January 1, 2001. Also, a deprecated + * constructor for StreamTokenizer was replaced, and the error messages in the catch + * clause were changed. Methods to get the field strength and inclination + * were added.

+ *

+ * Found out some interesting information about the altitude. The altitude entered + * for the calculations is the height above the WGS84 spheroid, not height MSL. Using + * MSL height means that the altitude could be in error by as much as 200 meters. + * This should not be significant for our applications.

+ * + *

NOTE: This class is not thread safe.

+ * + * @version 5.9 January 6, 2020 + *

Updated the internal coefficients to the 2020 epoch values. Passes the new JUnit tests.

+ * + * + *
    References: + * + *
  • JOHN M. QUINN, DAVID J. KERRIDGE AND DAVID R. BARRACLOUGH, + * WORLD MAGNETIC CHARTS FOR 1985 - SPHERICAL HARMONIC + * MODELS OF THE GEOMAGNETIC FIELD AND ITS SECULAR + * VARIATION, GEOPHYS. J. R. ASTR. SOC. (1986) 87, + * PP 1143-1157
  • + * + *
  • DEFENSE MAPPING AGENCY TECHNICAL REPORT, TR 8350.2: + * DEPARTMENT OF DEFENSE WORLD GEODETIC SYSTEM 1984, + * SEPT. 30 (1987)
  • + * + *
  • JOSEPH C. CAIN, ET AL.; A PROPOSED MODEL FOR THE + * INTERNATIONAL GEOMAGNETIC REFERENCE FIELD - 1965, + * J. GEOMAG. AND GEOELECT. VOL. 19, NO. 4, PP 335-355 + * (1967) (SEE APPENDIX)
  • + * + *
  • ALFRED J. ZMUDA, WORLD MAGNETIC SURVEY 1957-1969, + * INTERNATIONAL ASSOCIATION OF GEOMAGNETISM AND + * AERONOMY (IAGA) BULLETIN #28, PP 186-188 (1971)
  • + * + *
  • JOHN M. QUINN, RACHEL J. COLEMAN, MICHAEL R. PECK, AND + * STEPHEN E. LAUBER; THE JOINT US/UK 1990 EPOCH + * WORLD MAGNETIC MODEL, TECHNICAL REPORT NO. 304, + * NAVAL OCEANOGRAPHIC OFFICE (1991)
  • + * + *
  • JOHN M. QUINN, RACHEL J. COLEMAN, DONALD L. SHIEL, AND + * JOHN M. NIGRO; THE JOINT US/UK 1995 EPOCH WORLD + * MAGNETIC MODEL, TECHNICAL REPORT NO. 314, NAVAL + * OCEANOGRAPHIC OFFICE (1995)
+ * + * + * + * + *

WMM-2000 is a National Imagery and Mapping Agency (NIMA) standard + * product. It is covered under NIMA Military Specification: + * MIL-W-89500 (1993). + *

+ * For information on the use and applicability of this product contact

+ *

+ * DIRECTOR
+ * NATIONAL IMAGERY AND MAPPING AGENCY/HEADQUARTERS
+ * ATTN: CODE P33
+ * 12310 SUNRISE VALLEY DRIVE
+ * RESTON, VA 20191-3449
+ * (703) 264-3002
+ * + * + *

The FORTRAN version of GEOMAG PROGRAMMED BY:

+ *

+ * JOHN M. QUINN 7/19/90
+ * FLEET PRODUCTS DIVISION, CODE N342
+ * NAVAL OCEANOGRAPHIC OFFICE (NAVOCEANO)
+ * STENNIS SPACE CENTER (SSC), MS 39522-5001
+ * USA
+ * PHONE: COM: (601) 688-5828
+ * AV: 485-5828
+ * FAX: (601) 688-5521
+ * + *

NOW AT:

+ *

+ * GEOMAGNETICS GROUP
+ * U. S. GEOLOGICAL SURVEY MS 966
+ * FEDERAL CENTER
+ * DENVER, CO 80225-0046
+ * USA
+ * PHONE: COM: (303) 273-8475
+ * FAX: (303) 273-8600
+ * EMAIL: quinn@ghtmail.cr.usgs.gov
+ */ +public class TSAGeoMag { + /** A logger for this class. Every class MUST have this field, if you want to log from this class. + * The class name is the fully qualified class name of the class, such as java.lang.String. If you're not going + * to use log4j, then comment all references to the logger, and uncomment the System.***.println statements.*/ + //private static Logger logger = Logger.getLogger(TSAGeoMag.class); + + //variables for magnetic calculations //////////////////////////////////// + // + // Variables were identified in geomag.for, the FORTRAN + // version of the geomag calculator. + + /** + * The input string array which contains each line of input for the + * wmm.cof input file. Added so that all data was internal, so that + * applications do not have to mess with carrying around a data file. + * In the TSAGeoMag Class, the columns in this file are as follows: + * n, m, gnm, hnm, dgnm, dhnm + */ + private String[] input = + {" 2020.0 WMM-2020 12/10/2019", + " 1 0 -29404.5 0.0 6.7 0.0", + " 1 1 -1450.7 4652.9 7.7 -25.1", + " 2 0 -2500.0 0.0 -11.5 0.0", + " 2 1 2982.0 -2991.6 -7.1 -30.2", + " 2 2 1676.8 -734.8 -2.2 -23.9", + " 3 0 1363.9 0.0 2.8 0.0", + " 3 1 -2381.0 -82.2 -6.2 5.7", + " 3 2 1236.2 241.8 3.4 -1.0", + " 3 3 525.7 -542.9 -12.2 1.1", + " 4 0 903.1 0.0 -1.1 0.0", + " 4 1 809.4 282.0 -1.6 0.2", + " 4 2 86.2 -158.4 -6.0 6.9", + " 4 3 -309.4 199.8 5.4 3.7", + " 4 4 47.9 -350.1 -5.5 -5.6", + " 5 0 -234.4 0.0 -0.3 0.0", + " 5 1 363.1 47.7 0.6 0.1", + " 5 2 187.8 208.4 -0.7 2.5", + " 5 3 -140.7 -121.3 0.1 -0.9", + " 5 4 -151.2 32.2 1.2 3.0", + " 5 5 13.7 99.1 1.0 0.5", + " 6 0 65.9 0.0 -0.6 0.0", + " 6 1 65.6 -19.1 -0.4 0.1", + " 6 2 73.0 25.0 0.5 -1.8", + " 6 3 -121.5 52.7 1.4 -1.4", + " 6 4 -36.2 -64.4 -1.4 0.9", + " 6 5 13.5 9.0 -0.0 0.1", + " 6 6 -64.7 68.1 0.8 1.0", + " 7 0 80.6 0.0 -0.1 0.0", + " 7 1 -76.8 -51.4 -0.3 0.5", + " 7 2 -8.3 -16.8 -0.1 0.6", + " 7 3 56.5 2.3 0.7 -0.7", + " 7 4 15.8 23.5 0.2 -0.2", + " 7 5 6.4 -2.2 -0.5 -1.2", + " 7 6 -7.2 -27.2 -0.8 0.2", + " 7 7 9.8 -1.9 1.0 0.3", + " 8 0 23.6 0.0 -0.1 0.0", + " 8 1 9.8 8.4 0.1 -0.3", + " 8 2 -17.5 -15.3 -0.1 0.7", + " 8 3 -0.4 12.8 0.5 -0.2", + " 8 4 -21.1 -11.8 -0.1 0.5", + " 8 5 15.3 14.9 0.4 -0.3", + " 8 6 13.7 3.6 0.5 -0.5", + " 8 7 -16.5 -6.9 0.0 0.4", + " 8 8 -0.3 2.8 0.4 0.1", + " 9 0 5.0 0.0 -0.1 0.0", + " 9 1 8.2 -23.3 -0.2 -0.3", + " 9 2 2.9 11.1 -0.0 0.2", + " 9 3 -1.4 9.8 0.4 -0.4", + " 9 4 -1.1 -5.1 -0.3 0.4", + " 9 5 -13.3 -6.2 -0.0 0.1", + " 9 6 1.1 7.8 0.3 -0.0", + " 9 7 8.9 0.4 -0.0 -0.2", + " 9 8 -9.3 -1.5 -0.0 0.5", + " 9 9 -11.9 9.7 -0.4 0.2", + " 10 0 -1.9 0.0 0.0 0.0", + " 10 1 -6.2 3.4 -0.0 -0.0", + " 10 2 -0.1 -0.2 -0.0 0.1", + " 10 3 1.7 3.5 0.2 -0.3", + " 10 4 -0.9 4.8 -0.1 0.1", + " 10 5 0.6 -8.6 -0.2 -0.2", + " 10 6 -0.9 -0.1 -0.0 0.1", + " 10 7 1.9 -4.2 -0.1 -0.0", + " 10 8 1.4 -3.4 -0.2 -0.1", + " 10 9 -2.4 -0.1 -0.1 0.2", + " 10 10 -3.9 -8.8 -0.0 -0.0", + " 11 0 3.0 0.0 -0.0 0.0", + " 11 1 -1.4 -0.0 -0.1 -0.0", + " 11 2 -2.5 2.6 -0.0 0.1", + " 11 3 2.4 -0.5 0.0 0.0", + " 11 4 -0.9 -0.4 -0.0 0.2", + " 11 5 0.3 0.6 -0.1 -0.0", + " 11 6 -0.7 -0.2 0.0 0.0", + " 11 7 -0.1 -1.7 -0.0 0.1", + " 11 8 1.4 -1.6 -0.1 -0.0", + " 11 9 -0.6 -3.0 -0.1 -0.1", + " 11 10 0.2 -2.0 -0.1 0.0", + " 11 11 3.1 -2.6 -0.1 -0.0", + " 12 0 -2.0 0.0 0.0 0.0", + " 12 1 -0.1 -1.2 -0.0 -0.0", + " 12 2 0.5 0.5 -0.0 0.0", + " 12 3 1.3 1.3 0.0 -0.1", + " 12 4 -1.2 -1.8 -0.0 0.1", + " 12 5 0.7 0.1 -0.0 -0.0", + " 12 6 0.3 0.7 0.0 0.0", + " 12 7 0.5 -0.1 -0.0 -0.0", + " 12 8 -0.2 0.6 0.0 0.1", + " 12 9 -0.5 0.2 -0.0 -0.0", + " 12 10 0.1 -0.9 -0.0 -0.0", + " 12 11 -1.1 -0.0 -0.0 0.0", + " 12 12 -0.3 0.5 -0.1 -0.1" + }; + + /** + * Geodetic altitude in km. An input, + * but set to zero in this class. Changed + * back to an input in version 5. If not specified, + * then is 0. + */ + private double alt = 0; + + /** + * Geodetic latitude in deg. An input. + */ + private double glat = 0; + + /** + * Geodetic longitude in deg. An input. + */ + private double glon = 0; + + /** + * Time in decimal years. An input. + */ + private double time = 0; + + /** + * Geomagnetic declination in deg. + * East is positive, West is negative. + * (The negative of variation.) + */ + private double dec = 0; + + /** + * Geomagnetic inclination in deg. + * Down is positive, up is negative. + */ + private double dip = 0; + /** + * Geomagnetic total intensity, in nano Teslas. + */ + private double ti = 0; + + /** + * Geomagnetic grid variation, referenced to + * grid North. Not calculated or output in version 5.0. + */ + //private double gv = 0; + + /** + * The maximum number of degrees of the spherical harmonic model. + */ + private int maxdeg = 12; + + /** + * The maximum order of spherical harmonic model. + */ + private int maxord; + + /** + * Added in version 5. In earlier versions the date for the calculation was held as a + * constant. Now the default date is set to 2.5 years plus the epoch read from the + * input file. + */ + private double defaultDate = 2022.5; + + /** + * Added in version 5. In earlier versions the altitude for the calculation was held as a + * constant at 0. In version 5, if no altitude is specified in the calculation, this + * altitude is used by default. + */ + private final double defaultAltitude = 0; + + /** + * The Gauss coefficients of main geomagnetic model (nt). + */ + private double c[][] = new double[13][13]; + + /** + * The Gauss coefficients of secular geomagnetic model (nt/yr). + */ + private double cd[][] = new double[13][13]; + + /** + * The time adjusted geomagnetic gauss coefficients (nt). + */ + private double tc[][] = new double[13][13]; + + /** + * The theta derivative of p(n,m) (unnormalized). + */ + private double dp[][] = new double[13][13]; + + /** + * The Schmidt normalization factors. + */ + private double snorm[] = new double[169]; + + /** + * The sine of (m*spherical coord. longitude). + */ + private double sp[] = new double[13]; + + /** + * The cosine of (m*spherical coord. longitude). + */ + private double cp[] = new double[13]; + private double fn[] = new double[13]; + private double fm[] = new double[13]; + + /** + * The associated Legendre polynomials for m=1 (unnormalized). + */ + private double pp[] = new double[13]; + + private double k[][] = new double[13][13]; + + /** + * The variables otime (old time), oalt (old altitude), + * olat (old latitude), olon (old longitude), are used to + * store the values used from the previous calculation to + * save on calculation time if some inputs don't change. + */ + private double otime, oalt, olat, olon; + + /** + * The date in years, for the start of the valid time of the fit coefficients + */ + private double epoch; + + /** + * bx is the north south field intensity + * by is the east west field intensity + * bz is the vertical field intensity positive downward + * bh is the horizontal field intensity + */ + private double bx, by, bz, bh; + + /** + * re is the Mean radius of IAU-66 ellipsoid, in km. + * a2 is the Semi-major axis of WGS-84 ellipsoid, in km, squared. + * b2 is the Semi-minor axis of WGS-84 ellipsoid, in km, squared. + * c2 is c2 = a2 - b2 + * a4 is a2 squared. + * b4 is b2 squared. + * c4 is c4 = a4 - b4. + */ + private double re, a2, b2, c2, a4, b4, c4; + + private double r, d, ca, sa, ct, st; // even though these only occur in one method, they must be + // created here, or won't have correct values calculated + // These values are only recalculated if the altitude changes. + + // + //////////////////////////////////////////////////////////////////////////// + + /** + * Instantiates object by calling initModel(). + */ + public TSAGeoMag() { + //read model data from file and initialize the GeoMag routine + initModel(); + } + + /** + * Reads data from file and initializes magnetic model. If + * the file is not present, or an IO exception occurs, then the internal + * values valid for 2015 will be used. Note that the last line of the + * WMM.COF file must be 9999... for this method to read in the input + * file properly. + */ + private void initModel() { + glat = 0; + glon = 0; + //bOutDated = false; + //String strModel = new String(); + //String strFile = new String("WMM.COF"); + // String strFile = new String("wmm-95.dat"); + + // INITIALIZE CONSTANTS + maxord = maxdeg; + sp[0] = 0.0; + cp[0] = snorm[0] = pp[0] = 1.0; + dp[0][0] = 0.0; + /** + * Semi-major axis of WGS-84 ellipsoid, in km. + */ + double a = 6378.137; + /** + * Semi-minor axis of WGS-84 ellipsoid, in km. + */ + double b = 6356.7523142; + /** + * Mean radius of IAU-66 ellipsoid, in km. + */ + re = 6371.2; + a2 = a * a; + b2 = b * b; + c2 = a2 - b2; + a4 = a2 * a2; + b4 = b2 * b2; + c4 = a4 - b4; + + try { + //open data file and parse values + //InputStream is; + Reader is; + + InputStream input = getClass().getResourceAsStream("WMM.COF"); + if (input == null) throw new FileNotFoundException("WMM.COF not found"); + is = new InputStreamReader(input); + StreamTokenizer str = new StreamTokenizer(is); + + + // READ WORLD MAGNETIC MODEL SPHERICAL HARMONIC COEFFICIENTS + c[0][0] = 0.0; + cd[0][0] = 0.0; + str.nextToken(); + epoch = str.nval; + defaultDate = epoch + 2.5; + //logger.debug("TSAGeoMag Epoch is: " + epoch); + //logger.debug("TSAGeoMag default date is: " + defaultDate); + str.nextToken(); + //strModel = str.sval; + str.nextToken(); + + //loop to get data from file + while (true) { + str.nextToken(); + if (str.nval >= 9999) // end of file + break; + + int n = (int) str.nval; + str.nextToken(); + int m = (int) str.nval; + str.nextToken(); + double gnm = str.nval; + str.nextToken(); + double hnm = str.nval; + str.nextToken(); + double dgnm = str.nval; + str.nextToken(); + double dhnm = str.nval; + + if (m <= n) { + c[m][n] = gnm; + cd[m][n] = dgnm; + + if (m != 0) { + c[n][m - 1] = hnm; + cd[n][m - 1] = dhnm; + } + } + + } //while(true) + + is.close(); + } //try + // version 2, catch FileNotFound and IO exceptions separately, + // rather than catching all exceptions. + // Version 5.4 add logger support, and comment out System.out.println + catch (FileNotFoundException e) { + String msg = "\nNOTICE NOTICE NOTICE \n" + + "WMMCOF file not found in TSAGeoMag.InitModel()\n" + + "The input file WMM.COF was not found in the same\n" + + "directory as the application.\n" + + "The magnetic field components are set to internal values.\n"; + //logger.warn(msg, e); + + /* String message = new String(e.toString()); + + System.out.println("\nNOTICE NOTICE NOTICE "); + System.out.println("Error: " + message); + System.out.println("Error in TSAGeoMag.InitModel()"); + System.out.println("The input file WMM.COF was not found in the same"); + System.out.println("directory as the application."); + System.out.println("The magnetic field components are set to internal values."); + */ + setCoeff(); + } catch (IOException e) { + String msg = "\nNOTICE NOTICE NOTICE \n" + + "Problem reading the WMMCOF file in TSAGeoMag.InitModel()\n" + + "The input file WMM.COF was found, but there was a problem \n" + + "reading the data.\n" + + "The magnetic field components are set to internal values."; + + //logger.warn(msg, e); + + /* String message = new String(e.toString()); + System.out.println("\nNOTICE NOTICE NOTICE "); + System.out.println("Error: " + message); + System.out.println("Error in TSAGeoMag.InitModel()"); + System.out.println("The input file WMM.COF was found, but there was a problem "); + System.out.println("reading the data."); + System.out.println("The magnetic field components are set to internal values."); + + */ + setCoeff(); + } + // CONVERT SCHMIDT NORMALIZED GAUSS COEFFICIENTS TO UNNORMALIZED + snorm[0] = 1.0; + for (int n = 1; n <= maxord; n++) { + + snorm[n] = snorm[n - 1] * (2 * n - 1) / n; + int j = 2; + + for (int m = 0, D1 = 1, D2 = (n - m + D1) / D1; D2 > 0; D2--, m += D1) { + k[m][n] = (double) (((n - 1) * (n - 1)) - (m * m)) / (double) ((2 * n - 1) * (2 * n - 3)); + if (m > 0) { + double flnmj = ((n - m + 1) * j) / (double) (n + m); + snorm[n + m * 13] = snorm[n + (m - 1) * 13] * Math.sqrt(flnmj); + j = 1; + c[n][m - 1] = snorm[n + m * 13] * c[n][m - 1]; + cd[n][m - 1] = snorm[n + m * 13] * cd[n][m - 1]; + } + c[m][n] = snorm[n + m * 13] * c[m][n]; + cd[m][n] = snorm[n + m * 13] * cd[m][n]; + } //for(m...) + + fn[n] = (n + 1); + fm[n] = n; + + } //for(n...) + + k[1][1] = 0.0; + + otime = oalt = olat = olon = -1000.0; + + + } + + /** + *

PURPOSE: THIS ROUTINE COMPUTES THE DECLINATION (DEC), + * INCLINATION (DIP), TOTAL INTENSITY (TI) AND + * GRID VARIATION (GV - POLAR REGIONS ONLY, REFERENCED + * TO GRID NORTH OF POLAR STEREOGRAPHIC PROJECTION) OF + * THE EARTH'S MAGNETIC FIELD IN GEODETIC COORDINATES + * FROM THE COEFFICIENTS OF THE CURRENT OFFICIAL + * DEPARTMENT OF DEFENSE (DOD) SPHERICAL HARMONIC WORLD + * MAGNETIC MODEL (WMM-2010). THE WMM SERIES OF MODELS IS + * UPDATED EVERY 5 YEARS ON JANUARY 1'ST OF THOSE YEARS + * WHICH ARE DIVISIBLE BY 5 (I.E. 1980, 1985, 1990 ETC.) + * BY THE NAVAL OCEANOGRAPHIC OFFICE IN COOPERATION + * WITH THE BRITISH GEOLOGICAL SURVEY (BGS). THE MODEL + * IS BASED ON GEOMAGNETIC SURVEY MEASUREMENTS FROM + * AIRCRAFT, SATELLITE AND GEOMAGNETIC OBSERVATORIES.

+ * + * + * + * ACCURACY: IN OCEAN AREAS AT THE EARTH'S SURFACE OVER THE + * ENTIRE 5 YEAR LIFE OF A DEGREE AND ORDER 12 + * SPHERICAL HARMONIC MODEL SUCH AS WMM-95, THE ESTIMATED + * RMS ERRORS FOR THE VARIOUS MAGENTIC COMPONENTS ARE:

+ *
    + * DEC - 0.5 Degrees
    + * DIP - 0.5 Degrees
    + * TI - 280.0 nanoTeslas (nT)
    + * GV - 0.5 Degrees
+ * + *

OTHER MAGNETIC COMPONENTS THAT CAN BE DERIVED FROM + * THESE FOUR BY SIMPLE TRIGONOMETRIC RELATIONS WILL + * HAVE THE FOLLOWING APPROXIMATE ERRORS OVER OCEAN AREAS:

+ *
    + * X - 140 nT (North)
    + * Y - 140 nT (East)
    + * Z - 200 nT (Vertical) Positive is down
    + * H - 200 nT (Horizontal)
+ * + *

OVER LAND THE RMS ERRORS ARE EXPECTED TO BE SOMEWHAT + * HIGHER, ALTHOUGH THE RMS ERRORS FOR DEC, DIP AND GV + * ARE STILL ESTIMATED TO BE LESS THAN 0.5 DEGREE, FOR + * THE ENTIRE 5-YEAR LIFE OF THE MODEL AT THE EARTH's + * SURFACE. THE OTHER COMPONENT ERRORS OVER LAND ARE + * MORE DIFFICULT TO ESTIMATE AND SO ARE NOT GIVEN.

+ *

+ * THE ACCURACY AT ANY GIVEN TIME OF ALL FOUR + * GEOMAGNETIC PARAMETERS DEPENDS ON THE GEOMAGNETIC + * LATITUDE. THE ERRORS ARE LEAST AT THE EQUATOR AND + * GREATEST AT THE MAGNETIC POLES.

+ *

+ * IT IS VERY IMPORTANT TO NOTE THAT A DEGREE AND + * ORDER 12 MODEL, SUCH AS WMM-2010 DESCRIBES ONLY + * THE LONG WAVELENGTH SPATIAL MAGNETIC FLUCTUATIONS + * DUE TO EARTH'S CORE. NOT INCLUDED IN THE WMM SERIES + * MODELS ARE INTERMEDIATE AND SHORT WAVELENGTH + * SPATIAL FLUCTUATIONS OF THE GEOMAGNETIC FIELD + * WHICH ORIGINATE IN THE EARTH'S MANTLE AND CRUST. + * CONSEQUENTLY, ISOLATED ANGULAR ERRORS AT VARIOUS + * POSITIONS ON THE SURFACE (PRIMARILY OVER LAND, IN + * CONTINENTAL MARGINS AND OVER OCEANIC SEAMOUNTS, + * RIDGES AND TRENCHES) OF SEVERAL DEGREES MAY BE + * EXPECTED. ALSO NOT INCLUDED IN THE MODEL ARE + * NONSECULAR TEMPORAL FLUCTUATIONS OF THE GEOMAGNETIC + * FIELD OF MAGNETOSPHERIC AND IONOSPHERIC ORIGIN. + * DURING MAGNETIC STORMS, TEMPORAL FLUCTUATIONS CAN + * CAUSE SUBSTANTIAL DEVIATIONS OF THE GEOMAGNETIC + * FIELD FROM MODEL VALUES. IN ARCTIC AND ANTARCTIC + * REGIONS, AS WELL AS IN EQUATORIAL REGIONS, DEVIATIONS + * FROM MODEL VALUES ARE BOTH FREQUENT AND PERSISTENT.

+ *

+ * IF THE REQUIRED DECLINATION ACCURACY IS MORE + * STRINGENT THAN THE WMM SERIES OF MODELS PROVIDE, THEN + * THE USER IS ADVISED TO REQUEST SPECIAL (REGIONAL OR + * LOCAL) SURVEYS BE PERFORMED AND MODELS PREPARED BY + * THE USGS, WHICH OPERATES THE US GEOMAGNETIC + * OBSERVATORIES. REQUESTS OF THIS NATURE SHOULD + * BE MADE THROUGH NIMA AT THE ADDRESS ABOVE.

+ *

+ *

+ *

+ * NOTE: THIS VERSION OF GEOMAG USES THE WMM-2010 GEOMAGNETIC + * MODEL REFERENCED TO THE WGS-84 GRAVITY MODEL ELLIPSOID

+ * + * @param fLat The latitude in decimal degrees. + * @param fLon The longitude in decimal degrees. + * @param year The date as a decimal year. + * @param altitude The altitude in kilometers. + */ + private void calcGeoMag(double fLat, double fLon, double year, double altitude) { + + glat = fLat; + glon = fLon; + alt = altitude; + /** + * The date in decimal years for calculating the magnetic field components. + */ + time = year; + + double dt = time - epoch; + //if (otime < 0.0 && (dt < 0.0 || dt > 5.0)) + // if(bCurrent){ + // if (dt < 0.0 || dt > 5.0) + // bOutDated = true; + // else + // bOutDated = false; + // } + + double pi = Math.PI; + double dtr = (pi / 180.0); + double rlon = glon * dtr; + double rlat = glat * dtr; + double srlon = Math.sin(rlon); + double srlat = Math.sin(rlat); + double crlon = Math.cos(rlon); + double crlat = Math.cos(rlat); + double srlat2 = srlat * srlat; + double crlat2 = crlat * crlat; + sp[1] = srlon; + cp[1] = crlon; + + // CONVERT FROM GEODETIC COORDS. TO SPHERICAL COORDS. + if (alt != oalt || glat != olat) { + double q = Math.sqrt(a2 - c2 * srlat2); + double q1 = alt * q; + double q2 = ((q1 + a2) / (q1 + b2)) * ((q1 + a2) / (q1 + b2)); + ct = srlat / Math.sqrt(q2 * crlat2 + srlat2); + st = Math.sqrt(1.0 - (ct * ct)); + double r2 = ((alt * alt) + 2.0 * q1 + (a4 - c4 * srlat2) / (q * q)); + r = Math.sqrt(r2); + d = Math.sqrt(a2 * crlat2 + b2 * srlat2); + ca = (alt + d) / r; + sa = c2 * crlat * srlat / (r * d); + } + if (glon != olon) { + for (int m = 2; m <= maxord; m++) { + sp[m] = sp[1] * cp[m - 1] + cp[1] * sp[m - 1]; + cp[m] = cp[1] * cp[m - 1] - sp[1] * sp[m - 1]; + } + } + double aor = re / r; + double ar = aor * aor; + double br = 0, bt = 0, bp = 0, bpp = 0; + + for (int n = 1; n <= maxord; n++) { + ar = ar * aor; + for (int m = 0, D3 = 1, D4 = (n + m + D3) / D3; D4 > 0; D4--, m += D3) { + + //COMPUTE UNNORMALIZED ASSOCIATED LEGENDRE POLYNOMIALS + //AND DERIVATIVES VIA RECURSION RELATIONS + if (alt != oalt || glat != olat) { + if (n == m) { + snorm[n + m * 13] = st * snorm[n - 1 + (m - 1) * 13]; + dp[m][n] = st * dp[m - 1][n - 1] + ct * snorm[n - 1 + (m - 1) * 13]; + } + if (n == 1 && m == 0) { + snorm[n + m * 13] = ct * snorm[n - 1 + m * 13]; + dp[m][n] = ct * dp[m][n - 1] - st * snorm[n - 1 + m * 13]; + } + if (n > 1 && n != m) { + if (m > n - 2) + snorm[n - 2 + m * 13] = 0.0; + if (m > n - 2) + dp[m][n - 2] = 0.0; + snorm[n + m * 13] = ct * snorm[n - 1 + m * 13] - k[m][n] * snorm[n - 2 + m * 13]; + dp[m][n] = ct * dp[m][n - 1] - st * snorm[n - 1 + m * 13] - k[m][n] * dp[m][n - 2]; + } + } + + //TIME ADJUST THE GAUSS COEFFICIENTS + + if (time != otime) { + tc[m][n] = c[m][n] + dt * cd[m][n]; + + if (m != 0) + tc[n][m - 1] = c[n][m - 1] + dt * cd[n][m - 1]; + } + + //ACCUMULATE TERMS OF THE SPHERICAL HARMONIC EXPANSIONS + double temp1, temp2; + double par = ar * snorm[n + m * 13]; + if (m == 0) { + temp1 = tc[m][n] * cp[m]; + temp2 = tc[m][n] * sp[m]; + } else { + temp1 = tc[m][n] * cp[m] + tc[n][m - 1] * sp[m]; + temp2 = tc[m][n] * sp[m] - tc[n][m - 1] * cp[m]; + } + + bt = bt - ar * temp1 * dp[m][n]; + bp += (fm[m] * temp2 * par); + br += (fn[n] * temp1 * par); + + //SPECIAL CASE: NORTH/SOUTH GEOGRAPHIC POLES + + if (st == 0.0 && m == 1) { + if (n == 1) + pp[n] = pp[n - 1]; + else + pp[n] = ct * pp[n - 1] - k[m][n] * pp[n - 2]; + double parp = ar * pp[n]; + bpp += (fm[m] * temp2 * parp); + } + + } //for(m...) + + } //for(n...) + + + if (st == 0.0) + bp = bpp; + else + bp /= st; + + //ROTATE MAGNETIC VECTOR COMPONENTS FROM SPHERICAL TO + //GEODETIC COORDINATES + // by is the east-west field component + // bx is the north-south field component + // bz is the vertical field component. + bx = -bt * ca - br * sa; + by = bp; + bz = bt * sa - br * ca; + + //COMPUTE DECLINATION (DEC), INCLINATION (DIP) AND + //TOTAL INTENSITY (TI) + + bh = Math.sqrt((bx * bx) + (by * by)); + ti = Math.sqrt((bh * bh) + (bz * bz)); + // Calculate the declination. + dec = (Math.atan2(by, bx) / dtr); + //logger.debug( "Dec is: " + dec ); + dip = (Math.atan2(bz, bh) / dtr); + + // This is the variation for grid navigation. + // Not used at this time. See St. Ledger for explanation. + //COMPUTE MAGNETIC GRID VARIATION IF THE CURRENT + //GEODETIC POSITION IS IN THE ARCTIC OR ANTARCTIC + //(I.E. GLAT > +55 DEGREES OR GLAT < -55 DEGREES) + // Grid North is referenced to the 0 Meridian of a polar + // stereographic projection. + + //OTHERWISE, SET MAGNETIC GRID VARIATION TO -999.0 + /* + gv = -999.0; + if (Math.abs(glat) >= 55.){ + if (glat > 0.0 && glon >= 0.0) + gv = dec-glon; + if (glat > 0.0 && glon < 0.0) + gv = dec + Math.abs(glon); + if (glat < 0.0 && glon >= 0.0) + gv = dec+glon; + if (glat < 0.0 && glon < 0.0) + gv = dec - Math.abs(glon); + if (gv > +180.0) + gv -= 360.0; + if (gv < -180.0) + gv += 360.0; + } + */ + otime = time; + oalt = alt; + olat = glat; + olon = glon; + + } + + /** + * Returns the declination from the Department of + * Defense geomagnetic model and data, in degrees. The + * magnetic heading + declination = true heading. The date and + * altitude are the defaults, of half way through the valid + * 5 year period, and 0 elevation. + * (True heading + variation = magnetic heading.) + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @return The declination in degrees. + */ + public double getDeclination(double dlat, double dlong) { + calcGeoMag(dlat, dlong, defaultDate, defaultAltitude); + return dec; + } + + /** + * Returns the declination from the Department of + * Defense geomagnetic model and data, in degrees. The + * magnetic heading + declination = true heading. + * (True heading + variation = magnetic heading.) + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @param year The date as a decimal year. + * @param altitude The altitude in kilometers. + * @return The declination in degrees. + */ + public double getDeclination(double dlat, double dlong, double year, double altitude) { + calcGeoMag(dlat, dlong, year, altitude); + return dec; + } + + /** + * Returns the magnetic field intensity from the + * Department of Defense geomagnetic model and data + * in nano Tesla. The date and + * altitude are the defaults, of half way through the valid + * 5 year period, and 0 elevation. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @return Magnetic field strength in nano Tesla. + */ + public double getIntensity(double dlat, double dlong) { + calcGeoMag(dlat, dlong, defaultDate, defaultAltitude); + return ti; + } + + /** + * Returns the magnetic field intensity from the + * Department of Defense geomagnetic model and data + * in nano Tesla. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @param year Date of the calculation in decimal years. + * @param altitude Altitude of the calculation in kilometers. + * @return Magnetic field strength in nano Tesla. + */ + public double getIntensity(double dlat, double dlong, double year, double altitude) { + calcGeoMag(dlat, dlong, year, altitude); + return ti; + } + + /** + * Returns the horizontal magnetic field intensity from the + * Department of Defense geomagnetic model and data + * in nano Tesla. The date and + * altitude are the defaults, of half way through the valid + * 5 year period, and 0 elevation. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @return The horizontal magnetic field strength in nano Tesla. + */ + public double getHorizontalIntensity(double dlat, double dlong) { + calcGeoMag(dlat, dlong, defaultDate, defaultAltitude); + return bh; + } + + /** + * Returns the horizontal magnetic field intensity from the + * Department of Defense geomagnetic model and data + * in nano Tesla. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @param year Date of the calculation in decimal years. + * @param altitude Altitude of the calculation in kilometers. + * @return The horizontal magnetic field strength in nano Tesla. + */ + public double getHorizontalIntensity(double dlat, double dlong, double year, double altitude) { + calcGeoMag(dlat, dlong, year, altitude); + return bh; + } + + /** + * Returns the vertical magnetic field intensity from the + * Department of Defense geomagnetic model and data + * in nano Tesla. The date and + * altitude are the defaults, of half way through the valid + * 5 year period, and 0 elevation. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @return The vertical magnetic field strength in nano Tesla. + */ + public double getVerticalIntensity(double dlat, double dlong) { + calcGeoMag(dlat, dlong, defaultDate, defaultAltitude); + return bz; + } + + /** + * Returns the vertical magnetic field intensity from the + * Department of Defense geomagnetic model and data + * in nano Tesla. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @param year Date of the calculation in decimal years. + * @param altitude Altitude of the calculation in kilometers. + * @return The vertical magnetic field strength in nano Tesla. + */ + public double getVerticalIntensity(double dlat, double dlong, double year, double altitude) { + calcGeoMag(dlat, dlong, year, altitude); + return bz; + } + + /** + * Returns the northerly magnetic field intensity from the + * Department of Defense geomagnetic model and data + * in nano Tesla. The date and + * altitude are the defaults, of half way through the valid + * 5 year period, and 0 elevation. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @return The northerly component of the magnetic field strength in nano Tesla. + */ + public double getNorthIntensity(double dlat, double dlong) { + calcGeoMag(dlat, dlong, defaultDate, defaultAltitude); + return bx; + } + + /** + * Returns the northerly magnetic field intensity from the + * Department of Defense geomagnetic model and data + * in nano Tesla. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @param year Date of the calculation in decimal years. + * @param altitude Altitude of the calculation in kilometers. + * @return The northerly component of the magnetic field strength in nano Tesla. + */ + public double getNorthIntensity(double dlat, double dlong, double year, double altitude) { + calcGeoMag(dlat, dlong, year, altitude); + return bx; + } + + /** + * Returns the easterly magnetic field intensity from the + * Department of Defense geomagnetic model and data + * in nano Tesla. The date and + * altitude are the defaults, of half way through the valid + * 5 year period, and 0 elevation. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @return The easterly component of the magnetic field strength in nano Tesla. + */ + public double getEastIntensity(double dlat, double dlong) { + calcGeoMag(dlat, dlong, defaultDate, defaultAltitude); + return by; + } + + /** + * Returns the easterly magnetic field intensity from the + * Department of Defense geomagnetic model and data + * in nano Tesla. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @param year Date of the calculation in decimal years. + * @param altitude Altitude of the calculation in kilometers. + * @return The easterly component of the magnetic field strength in nano Tesla. + */ + public double getEastIntensity(double dlat, double dlong, double year, double altitude) { + calcGeoMag(dlat, dlong, year, altitude); + return by; + } + + /** + * Returns the magnetic field dip angle from the + * Department of Defense geomagnetic model and data, + * in degrees. The date and + * altitude are the defaults, of half way through the valid + * 5 year period, and 0 elevation. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @return The magnetic field dip angle, in degrees. + */ + public double getDipAngle(double dlat, double dlong) { + calcGeoMag(dlat, dlong, defaultDate, defaultAltitude); + return dip; + } + + /** + * Returns the magnetic field dip angle from the + * Department of Defense geomagnetic model and data, + * in degrees. + * + * @param dlong Longitude in decimal degrees. + * @param dlat Latitude in decimal degrees. + * @param year Date of the calculation in decimal years. + * @param altitude Altitude of the calculation in kilometers. + * @return The magnetic field dip angle, in degrees. + */ + public double getDipAngle(double dlat, double dlong, double year, double altitude) { + calcGeoMag(dlat, dlong, year, altitude); + return dip; + } + + /** + * This method sets the input data to the internal fit coefficents. + * If there is an exception reading the input file WMM.COF, these values + * are used. + *

+ * NOTE: This method is not tested by the JUnit test, unless the WMM.COF file + * is missing. + */ + private void setCoeff() { + c[0][0] = 0.0; + cd[0][0] = 0.0; + + epoch = Double.parseDouble(input[0].trim().split("[\\s]+")[0]); + defaultDate = epoch + 2.5; + + String[] tokens; + + //loop to get data from internal values + for (int i = 1; i < input.length; i++) { + tokens = input[i].trim().split("[\\s]+"); + + int n = Integer.parseInt(tokens[0]); + int m = Integer.parseInt(tokens[1]); + double gnm = Double.parseDouble(tokens[2]); + double hnm = Double.parseDouble(tokens[3]); + double dgnm = Double.parseDouble(tokens[4]); + double dhnm = Double.parseDouble(tokens[5]); + + if (m <= n) { + c[m][n] = gnm; + cd[m][n] = dgnm; + + if (m != 0) { + c[n][m - 1] = hnm; + cd[n][m - 1] = dhnm; + } + } + } + } + + /** + *

+ * Given a Gregorian Calendar object, this returns the decimal year + * value for the calendar, accurate to the day of the input calendar. + * The hours, minutes, and seconds of the date are ignored.

+ *

+ * If the input Gregorian Calendar is new GregorianCalendar(2012, 6, 1), all of + * the first of July is counted, and this returns 2012.5. (183 days out of 366)

+ *

+ * If the input Gregorian Calendar is new GregorianCalendar(2010, 0, 0), the first + * of January is not counted, and this returns 2010.0

+ * + * @param cal Has the date (year, month, and day of the month) + * @return The date in decimal years + */ + public double decimalYear(GregorianCalendar cal) { + int year = cal.get(Calendar.YEAR); + double daysInYear; + if (cal.isLeapYear(year)) { + daysInYear = 366.0; + } else { + daysInYear = 365.0; + } + + return year + (cal.get(Calendar.DAY_OF_YEAR)) / daysInYear; + } +} diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies new file mode 100644 index 0000000..a48755f --- /dev/null +++ b/example/.flutter-plugins-dependencies @@ -0,0 +1 @@ +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"flutter_compass_v2","path":"C:\\\\Users\\\\Med Yassine Sabri\\\\Desktop\\\\projects\\\\flutter_compass_v2\\\\","native_build":true,"dependencies":[]},{"name":"permission_handler_apple","path":"C:\\\\Users\\\\Med Yassine Sabri\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_apple-9.4.4\\\\","native_build":true,"dependencies":[]}],"android":[{"name":"flutter_compass_v2","path":"C:\\\\Users\\\\Med Yassine Sabri\\\\Desktop\\\\projects\\\\flutter_compass_v2\\\\","native_build":true,"dependencies":[]},{"name":"permission_handler_android","path":"C:\\\\Users\\\\Med Yassine Sabri\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_android-12.0.5\\\\","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[{"name":"permission_handler_windows","path":"C:\\\\Users\\\\Med Yassine Sabri\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_windows-0.2.1\\\\","native_build":true,"dependencies":[]}],"web":[{"name":"permission_handler_html","path":"C:\\\\Users\\\\Med Yassine Sabri\\\\AppData\\\\Local\\\\Pub\\\\Cache\\\\hosted\\\\pub.dev\\\\permission_handler_html-0.1.1\\\\","dependencies":[]}]},"dependencyGraph":[{"name":"flutter_compass_v2","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]}],"date_created":"2024-03-28 11:12:00.496225","version":"3.19.3"} \ No newline at end of file diff --git a/example/.gitignore b/example/.gitignore index b7a0f31..1e9a1b3 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -1,10 +1,85 @@ +# Fastlane +fastlane/report.xml + +# Miscellaneous +*.class +*.lock +!Gemfile.lock +*.log +*.pyc +*.swp +.settings .DS_Store -.dart_tool/ +.atom/ +.buildlog/ +.history +.svn/ +.settings + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins .packages +.pub-cache/ .pub/ - build/ -.flutter-plugins -.flutter-plugins-dependencies +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* +Runner.app.dSYM.zip +Runner.ipa + + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages + +fastlane/README.md +.project +*.bak +*.classpath diff --git a/example/.idea/codeStyles/Project.xml b/example/.idea/codeStyles/Project.xml deleted file mode 100644 index 79a710f..0000000 --- a/example/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/example/.idea/codeStyles/codeStyleConfig.xml b/example/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index a55e7a1..0000000 --- a/example/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/example/.idea/libraries/Dart_SDK.xml b/example/.idea/libraries/Dart_SDK.xml deleted file mode 100644 index 64c6d58..0000000 --- a/example/.idea/libraries/Dart_SDK.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/example/.idea/libraries/Flutter_Plugins.xml b/example/.idea/libraries/Flutter_Plugins.xml deleted file mode 100644 index 83a5fa6..0000000 --- a/example/.idea/libraries/Flutter_Plugins.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/example/.idea/libraries/Flutter_for_Android.xml b/example/.idea/libraries/Flutter_for_Android.xml deleted file mode 100644 index 7e7ddf2..0000000 --- a/example/.idea/libraries/Flutter_for_Android.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/example/.idea/markdown-navigator.xml b/example/.idea/markdown-navigator.xml deleted file mode 100644 index 076726f..0000000 --- a/example/.idea/markdown-navigator.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/example/.idea/markdown-navigator/profiles_settings.xml b/example/.idea/markdown-navigator/profiles_settings.xml deleted file mode 100644 index 57927c5..0000000 --- a/example/.idea/markdown-navigator/profiles_settings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/example/.idea/modules.xml b/example/.idea/modules.xml deleted file mode 100644 index c59eace..0000000 --- a/example/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/example/.idea/runConfigurations/main_dart.xml b/example/.idea/runConfigurations/main_dart.xml deleted file mode 100644 index aab7b5c..0000000 --- a/example/.idea/runConfigurations/main_dart.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/example/.idea/workspace.xml b/example/.idea/workspace.xml deleted file mode 100644 index f208fc9..0000000 --- a/example/.idea/workspace.xml +++ /dev/null @@ -1,184 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -