diff --git a/app/build.gradle b/app/build.gradle
index 84fbf295..174ff387 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -119,6 +119,9 @@ dependencies {
implementation 'com.google.guava:guava:29.0-android'
implementation 'com.github.pengrad:mapscaleview:1.6.0'
+ // Android Auto
+ googleImplementation 'com.google.android.libraries.car:car-app:1.0.0-beta.1'
+
// AnyMaps
def anyMapsVersion = '1f050d860f'
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
diff --git a/app/src/google/AndroidManifest.xml b/app/src/google/AndroidManifest.xml
index 68e5ef13..0b26250d 100644
--- a/app/src/google/AndroidManifest.xml
+++ b/app/src/google/AndroidManifest.xml
@@ -1,10 +1,40 @@
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt
new file mode 100644
index 00000000..20fc6ce2
--- /dev/null
+++ b/app/src/google/java/net/vonforst/evmap/auto/CarAppService.kt
@@ -0,0 +1,246 @@
+package net.vonforst.evmap.auto
+
+import android.content.*
+import android.location.Location
+import android.os.IBinder
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import androidx.lifecycle.lifecycleScope
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.google.android.libraries.car.app.CarContext
+import com.google.android.libraries.car.app.Screen
+import com.google.android.libraries.car.app.model.*
+import com.google.android.libraries.car.app.model.Distance.UNIT_KILOMETERS
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.launch
+import net.vonforst.evmap.R
+import net.vonforst.evmap.api.availability.ChargeLocationStatus
+import net.vonforst.evmap.api.availability.ChargepointStatus
+import net.vonforst.evmap.api.availability.getAvailability
+import net.vonforst.evmap.api.goingelectric.ChargeLocation
+import net.vonforst.evmap.api.goingelectric.GoingElectricApi
+import net.vonforst.evmap.ui.availabilityText
+import net.vonforst.evmap.ui.getMarkerTint
+import net.vonforst.evmap.utils.distanceBetween
+import java.time.Duration
+import java.time.ZonedDateTime
+import kotlin.math.roundToInt
+
+
+class CarAppService : com.google.android.libraries.car.app.CarAppService(), LifecycleObserver {
+ private lateinit var mapScreen: MapScreen
+ private var locationService: CarLocationService? = null
+
+ private val serviceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName, ibinder: IBinder) {
+ val binder: CarLocationService.LocalBinder = ibinder as CarLocationService.LocalBinder
+ locationService = binder.service
+ locationService?.requestLocationUpdates()
+ }
+
+ override fun onServiceDisconnected(name: ComponentName?) {
+ locationService = null
+ }
+ }
+
+ private val locationReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val location = intent.getParcelableExtra(CarLocationService.EXTRA_LOCATION) as Location?
+ if (location != null) {
+ mapScreen.updateLocation(location)
+ }
+ }
+ }
+
+ override fun onCreate() {
+ mapScreen = MapScreen(carContext)
+ lifecycle.addObserver(this)
+ }
+
+ override fun onCreateScreen(intent: Intent): Screen {
+ return mapScreen
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_START)
+ private fun bindLocationService() {
+ bindService(
+ Intent(this, CarLocationService::class.java),
+ serviceConnection,
+ Context.BIND_AUTO_CREATE
+ )
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
+ private fun unbindLocationService() {
+ locationService?.removeLocationUpdates()
+ unbindService(serviceConnection)
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
+ private fun registerBroadcastReceiver() {
+ LocalBroadcastManager.getInstance(this).registerReceiver(
+ locationReceiver,
+ IntentFilter(CarLocationService.ACTION_BROADCAST)
+ );
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
+ private fun unregisterBroadcastReceiver() {
+ LocalBroadcastManager.getInstance(this).unregisterReceiver(locationReceiver)
+ }
+}
+
+class MapScreen(ctx: CarContext) : Screen(ctx) {
+ private var location: Location? = null
+ private var lastUpdateLocation: Location? = null
+ private var chargers: List? = null
+ private val api by lazy {
+ GoingElectricApi.create(ctx.getString(R.string.goingelectric_key), context = ctx)
+ }
+ private val searchRadius = 5 // kilometers
+ private val updateThreshold = 50 // meters
+ private val availabilityUpdateThreshold = Duration.ofMinutes(1)
+ private var availabilities: MutableMap> =
+ HashMap()
+ private val maxRows = 6
+
+ override fun getTemplate(): Template {
+ return PlaceListMapTemplate.builder().apply {
+ setTitle("EVMap")
+ location?.let {
+ setAnchor(Place.builder(LatLng.create(it)).build())
+ }
+ chargers?.take(maxRows)?.let { chargerList ->
+ val builder = ItemList.Builder()
+ chargerList.forEach { charger ->
+ builder.addItem(formatCharger(charger))
+ }
+ builder.setNoItemsMessage(carContext.getString(R.string.auto_no_chargers_found))
+ setItemList(builder.build())
+ }
+ if (chargers == null || location == null) setIsLoading(true)
+ setCurrentLocationEnabled(true)
+ build()
+ }.build()
+ }
+
+ private fun formatCharger(charger: ChargeLocation): Row {
+ /*val icon = CarIcon.builder(IconCompat.createWithResource(carContext, R.drawable.ic_map_marker_charging))
+ .setTint(CarColor.BLUE)
+ .build()*/
+ val color = ContextCompat.getColor(carContext, getMarkerTint(charger))
+ val place = Place.builder(LatLng.create(charger.coordinates.lat, charger.coordinates.lng))
+ .setMarker(
+ PlaceMarker.builder()
+ .setColor(CarColor.createCustom(color, color))
+ .build()
+ )
+ .build()
+
+ return Row.builder().apply {
+ setTitle(charger.name)
+ val text = SpannableStringBuilder()
+
+ // distance
+ location?.let {
+ val distance = distanceBetween(
+ it.latitude, it.longitude,
+ charger.coordinates.lat, charger.coordinates.lng
+ ) / 1000
+ text.append(
+ "distance",
+ DistanceSpan.create(Distance.create(distance, UNIT_KILOMETERS)),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+
+ // power
+ if (text.isNotEmpty()) text.append(" · ")
+ text.append("${charger.maxPower.roundToInt()} kW")
+
+ // availability
+ availabilities[charger.id]?.second?.let { av ->
+ val status = av.status.values.flatten()
+ val available = availabilityText(status)
+ val total = charger.chargepoints.sumBy { it.count }
+
+ if (text.isNotEmpty()) text.append(" · ")
+ text.append(
+ "$available/$total",
+ ForegroundCarColorSpan.create(carAvailabilityColor(status)),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+
+ addText(text)
+ setMetadata(Metadata.ofPlace(place))
+ }.build()
+ }
+
+ fun updateLocation(location: Location) {
+ this.location = location
+
+ if (lastUpdateLocation == null) invalidate()
+
+ if (lastUpdateLocation == null ||
+ location.distanceTo(lastUpdateLocation) > updateThreshold
+ ) {
+ lastUpdateLocation = location
+
+ // update displayed chargers
+ lifecycleScope.launch {
+ // load chargers
+ val response = api.getChargepointsRadius(
+ location.latitude,
+ location.longitude,
+ searchRadius,
+ zoom = 16f
+ )
+ chargers =
+ response.body()?.chargelocations?.filterIsInstance(ChargeLocation::class.java)
+
+ // remove outdated availabilities
+ availabilities = availabilities.filter {
+ Duration.between(
+ it.value.first,
+ ZonedDateTime.now()
+ ) > availabilityUpdateThreshold
+ }.toMutableMap()
+
+ // update availabilities
+ chargers?.take(maxRows)?.map {
+ lifecycleScope.async {
+ // update only if not yet stored
+ if (!availabilities.containsKey(it.id)) {
+ val date = ZonedDateTime.now()
+ val availability = getAvailability(it).data
+ if (availability != null) {
+ availabilities[it.id] = date to availability
+ }
+ }
+ }
+ }?.awaitAll()
+
+ invalidate()
+ }
+ }
+ }
+}
+
+fun carAvailabilityColor(status: List): CarColor {
+ val unknown = status.any { it == ChargepointStatus.UNKNOWN }
+ val available = status.count { it == ChargepointStatus.AVAILABLE }
+
+ return if (unknown) {
+ CarColor.DEFAULT
+ } else if (available > 0) {
+ CarColor.GREEN
+ } else {
+ CarColor.RED
+ }
+}
\ No newline at end of file
diff --git a/app/src/google/java/net/vonforst/evmap/auto/CarLocationService.kt b/app/src/google/java/net/vonforst/evmap/auto/CarLocationService.kt
new file mode 100644
index 00000000..48d15e9b
--- /dev/null
+++ b/app/src/google/java/net/vonforst/evmap/auto/CarLocationService.kt
@@ -0,0 +1,163 @@
+package net.vonforst.evmap.auto
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Intent
+import android.location.Location
+import android.os.*
+import android.util.Log
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.google.android.gms.location.*
+import net.vonforst.evmap.BuildConfig
+import net.vonforst.evmap.R
+
+
+class CarLocationService : Service() {
+ private lateinit var serviceHandler: Handler
+ private lateinit var locationRequest: LocationRequest
+ private lateinit var notificationManager: NotificationManager
+ private lateinit var locationCallback: LocationCallback
+ private lateinit var fusedLocationClient: FusedLocationProviderClient
+ private val binder: IBinder = LocalBinder(this)
+ private var location: Location? = null
+
+ private val UPDATE_INTERVAL_IN_MILLISECONDS = 10000L
+ private val FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS = UPDATE_INTERVAL_IN_MILLISECONDS / 2
+
+ private val CHANNEL_ID = "car_location"
+ private val NOTIFICATION_ID = 1000
+ private val TAG = "CarLocationService"
+
+ companion object {
+ const val ACTION_BROADCAST: String = BuildConfig.APPLICATION_ID + ".car_location_broadcast"
+ const val EXTRA_LOCATION: String = BuildConfig.APPLICATION_ID + ".location"
+ }
+
+ override fun onCreate() {
+ fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
+ locationCallback = object : LocationCallback() {
+ override fun onLocationResult(locationResult: LocationResult) {
+ super.onLocationResult(locationResult)
+ onNewLocation(locationResult.lastLocation)
+ }
+ }
+ createLocationRequest()
+ getLastLocation()
+ val handlerThread = HandlerThread(TAG)
+ handlerThread.start()
+ serviceHandler = Handler(handlerThread.looper)
+ notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
+
+ // Android O requires a Notification Channel.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val name: CharSequence = getString(R.string.app_name)
+ // Create the channel for the notification
+ val mChannel =
+ NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT)
+
+ // Set the Notification Channel for the Notification Manager.
+ notificationManager.createNotificationChannel(mChannel)
+ }
+
+ startForeground(NOTIFICATION_ID, getNotification())
+ }
+
+ /**
+ * Returns the [NotificationCompat] used as part of the foreground service.
+ */
+ private fun getNotification(): Notification {
+ val builder: NotificationCompat.Builder = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentText(getString(R.string.auto_location_service))
+ .setContentTitle(getString(R.string.app_name))
+ .setOngoing(true)
+ .setPriority(NotificationManagerCompat.IMPORTANCE_HIGH)
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setTicker(getString(R.string.auto_location_service))
+ .setWhen(System.currentTimeMillis())
+
+ return builder.build()
+ }
+
+ override fun onBind(intent: Intent?): IBinder {
+ return binder
+ }
+
+ private fun createLocationRequest() {
+ locationRequest = LocationRequest()
+ locationRequest.interval = UPDATE_INTERVAL_IN_MILLISECONDS
+ locationRequest.fastestInterval = FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS
+ locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
+ }
+
+ private fun onNewLocation(location: Location) {
+ Log.i(TAG, "New location: $location")
+ this.location = location
+
+ // Notify anyone listening for broadcasts about the new location.
+ val intent = Intent(ACTION_BROADCAST)
+ intent.putExtra(EXTRA_LOCATION, location)
+ LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
+ }
+
+ private fun getLastLocation() {
+ try {
+ fusedLocationClient.lastLocation
+ .addOnCompleteListener { task ->
+ if (task.isSuccessful && task.result != null) {
+ location = task.result
+ } else {
+ Log.w(TAG, "Failed to get location.")
+ }
+ }
+ } catch (unlikely: SecurityException) {
+ Log.e(TAG, "Lost location permission.$unlikely")
+ }
+ }
+
+ /**
+ * Makes a request for location updates. Note that in this sample we merely log the
+ * [SecurityException].
+ */
+ fun requestLocationUpdates() {
+ Log.i(TAG, "Requesting location updates")
+ startService(Intent(applicationContext, CarLocationService::class.java))
+ try {
+ fusedLocationClient.requestLocationUpdates(
+ locationRequest,
+ locationCallback, Looper.myLooper()
+ )
+ } catch (unlikely: SecurityException) {
+ Log.e(TAG, "Lost location permission. Could not request updates. $unlikely")
+ }
+ }
+
+ /**
+ * Removes location updates. Note that in this sample we merely log the
+ * [SecurityException].
+ */
+ fun removeLocationUpdates() {
+ Log.i(TAG, "Removing location updates")
+ try {
+ fusedLocationClient.removeLocationUpdates(locationCallback)
+ stopSelf()
+ } catch (unlikely: SecurityException) {
+ Log.e(TAG, "Lost location permission. Could not remove updates. $unlikely")
+ }
+ }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ Log.i(TAG, "Service started")
+ // Tells the system to not try to recreate the service after it has been killed.
+ return START_NOT_STICKY
+ }
+
+ override fun onDestroy() {
+ serviceHandler.removeCallbacksAndMessages(null)
+ }
+
+ class LocalBinder(val service: CarLocationService) : Binder()
+}
\ No newline at end of file
diff --git a/app/src/google/res/values-de/values.xml b/app/src/google/res/values-de/values.xml
index 6cbc6cc3..26e3cfb5 100644
--- a/app/src/google/res/values-de/values.xml
+++ b/app/src/google/res/values-de/values.xml
@@ -5,4 +5,6 @@
- OpenStreetMap (Mapbox)
Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.
+ EVMap läuft unter Android Auto und nutzt dafür deinen Standort.
+ Keine Ladestationen in der Nähe gefunden
\ No newline at end of file
diff --git a/app/src/google/res/values/styles.xml b/app/src/google/res/values/styles.xml
new file mode 100644
index 00000000..5c7db417
--- /dev/null
+++ b/app/src/google/res/values/styles.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/google/res/values/values.xml b/app/src/google/res/values/values.xml
index affb01ba..6bcd01ea 100644
--- a/app/src/google/res/values/values.xml
+++ b/app/src/google/res/values/values.xml
@@ -10,4 +10,6 @@
google
Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation.
+ EVMap is running on Android Auto and using your location.
+ No nearby chargers found
\ No newline at end of file
diff --git a/app/src/google/res/xml/automotive_app_desc.xml b/app/src/google/res/xml/automotive_app_desc.xml
new file mode 100644
index 00000000..3c5355ab
--- /dev/null
+++ b/app/src/google/res/xml/automotive_app_desc.xml
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a638be9a..71ce9597 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -54,6 +54,7 @@
+
\ No newline at end of file
diff --git a/app/src/main/java/net/vonforst/evmap/api/Utils.kt b/app/src/main/java/net/vonforst/evmap/api/Utils.kt
index 7ea69928..5478ea9f 100644
--- a/app/src/main/java/net/vonforst/evmap/api/Utils.kt
+++ b/app/src/main/java/net/vonforst/evmap/api/Utils.kt
@@ -37,28 +37,4 @@ suspend fun Call.await(): Response {
}
}
}
-}
-
-const val earthRadiusKm: Double = 6372.8
-
-/**
- * Calculates the distance between two points on Earth in meters.
- * Latitude and longitude should be given in degrees.
- */
-fun distanceBetween(
- startLatitude: Double, startLongitude: Double,
- endLatitude: Double, endLongitude: Double
-): Double {
- // see https://rosettacode.org/wiki/Haversine_formula#Java
- val dLat = Math.toRadians(endLatitude - startLatitude);
- val dLon = Math.toRadians(endLongitude - startLongitude);
- val originLat = Math.toRadians(startLatitude);
- val destinationLat = Math.toRadians(endLatitude);
-
- val a = Math.pow(Math.sin(dLat / 2), 2.toDouble()) + Math.pow(
- Math.sin(dLon / 2),
- 2.toDouble()
- ) * Math.cos(originLat) * Math.cos(destinationLat);
- val c = 2 * Math.asin(Math.sqrt(a));
- return earthRadiusKm * c * 1000;
}
\ No newline at end of file
diff --git a/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt b/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt
index 23e45c8f..350ac189 100644
--- a/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt
+++ b/app/src/main/java/net/vonforst/evmap/api/availability/NewMotionAvailabilityDetector.kt
@@ -1,9 +1,9 @@
package net.vonforst.evmap.api.availability
import com.squareup.moshi.JsonClass
-import net.vonforst.evmap.api.distanceBetween
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.Chargepoint
+import net.vonforst.evmap.utils.distanceBetween
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
diff --git a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt
index e075067a..9caab887 100644
--- a/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt
+++ b/app/src/main/java/net/vonforst/evmap/api/goingelectric/GoingElectricApi.kt
@@ -16,7 +16,7 @@ import retrofit2.http.Query
interface GoingElectricApi {
@GET("chargepoints/")
suspend fun getChargepoints(
- @Query("sw_lat") swlat: Double, @Query("sw_lng") sw_lng: Double,
+ @Query("sw_lat") sw_lat: Double, @Query("sw_lng") sw_lng: Double,
@Query("ne_lat") ne_lat: Double, @Query("ne_lng") ne_lng: Double,
@Query("zoom") zoom: Float,
@Query("clustering") clustering: Boolean = false,
@@ -34,6 +34,27 @@ interface GoingElectricApi {
@Query("exclude_faults") excludeFaults: Boolean = false
): Response
+ @GET("chargepoints/")
+ suspend fun getChargepointsRadius(
+ @Query("lat") lat: Double, @Query("lng") lng: Double,
+ @Query("radius") radius: Int,
+ @Query("zoom") zoom: Float,
+ @Query("orderby") orderby: String = "distance",
+ @Query("clustering") clustering: Boolean = false,
+ @Query("cluster_distance") clusterDistance: Int? = null,
+ @Query("freecharging") freecharging: Boolean = false,
+ @Query("freeparking") freeparking: Boolean = false,
+ @Query("min_power") minPower: Int = 0,
+ @Query("plugs") plugs: String? = null,
+ @Query("chargecards") chargecards: String? = null,
+ @Query("networks") networks: String? = null,
+ @Query("categories") categories: String? = null,
+ @Query("startkey") startkey: Int? = null,
+ @Query("open_twentyfourseven") open247: Boolean = false,
+ @Query("barrierfree") barrierfree: Boolean = false,
+ @Query("exclude_faults") excludeFaults: Boolean = false
+ ): Response
+
@GET("chargepoints/")
fun getChargepointDetail(@Query("ge_id") id: Long): Call
diff --git a/app/src/main/java/net/vonforst/evmap/ui/MarkerUtils.kt b/app/src/main/java/net/vonforst/evmap/ui/MarkerUtils.kt
index 04c62750..38f0f03e 100644
--- a/app/src/main/java/net/vonforst/evmap/ui/MarkerUtils.kt
+++ b/app/src/main/java/net/vonforst/evmap/ui/MarkerUtils.kt
@@ -12,7 +12,7 @@ import kotlin.math.max
fun getMarkerTint(
charger: ChargeLocation,
- connectors: Set?
+ connectors: Set? = null
): Int = when {
charger.maxPower(connectors) >= 100 -> R.color.charger_100kw
charger.maxPower(connectors) >= 43 -> R.color.charger_43kw
diff --git a/app/src/main/java/net/vonforst/evmap/utils/LocationUtils.kt b/app/src/main/java/net/vonforst/evmap/utils/LocationUtils.kt
new file mode 100644
index 00000000..42414d49
--- /dev/null
+++ b/app/src/main/java/net/vonforst/evmap/utils/LocationUtils.kt
@@ -0,0 +1,34 @@
+package net.vonforst.evmap.utils
+
+import android.location.Location
+import kotlin.math.*
+
+/**
+ * Adds a certain distance in meters to a location. Approximate calculation.
+ */
+fun Location.plusMeters(dx: Double, dy: Double): Pair {
+ val lat = this.latitude + (180 / Math.PI) * (dx / 6378137.0)
+ val lon = this.longitude + (180 / Math.PI) * (dy / 6378137.0) / cos(Math.toRadians(lat))
+ return Pair(lat, lon)
+}
+
+const val earthRadiusM = 6378137.0
+
+/**
+ * Calculates the distance between two points on Earth in meters.
+ * Latitude and longitude should be given in degrees.
+ */
+fun distanceBetween(
+ startLatitude: Double, startLongitude: Double,
+ endLatitude: Double, endLongitude: Double
+): Double {
+ // see https://rosettacode.org/wiki/Haversine_formula#Java
+ val dLat = Math.toRadians(endLatitude - startLatitude)
+ val dLon = Math.toRadians(endLongitude - startLongitude)
+ val originLat = Math.toRadians(startLatitude)
+ val destinationLat = Math.toRadians(endLatitude)
+
+ val a = sin(dLat / 2).pow(2.0) + sin(dLon / 2).pow(2.0) * cos(originLat) * cos(destinationLat)
+ val c = 2 * asin(sqrt(a))
+ return earthRadiusM * c
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt
index 9be638df..d969949d 100644
--- a/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt
+++ b/app/src/main/java/net/vonforst/evmap/viewmodel/FavoritesViewModel.kt
@@ -10,10 +10,10 @@ import net.vonforst.evmap.adapter.Equatable
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.ChargepointStatus
import net.vonforst.evmap.api.availability.getAvailability
-import net.vonforst.evmap.api.distanceBetween
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
import net.vonforst.evmap.storage.AppDatabase
+import net.vonforst.evmap.utils.distanceBetween
class FavoritesViewModel(application: Application, geApiKey: String) :
AndroidViewModel(application) {
diff --git a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt
index 9649a037..d33277ce 100644
--- a/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt
+++ b/app/src/main/java/net/vonforst/evmap/viewmodel/MapViewModel.kt
@@ -9,10 +9,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
-import net.vonforst.evmap.api.distanceBetween
import net.vonforst.evmap.api.goingelectric.*
import net.vonforst.evmap.storage.*
import net.vonforst.evmap.ui.cluster
+import net.vonforst.evmap.utils.distanceBetween
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
diff --git a/app/src/test/java/net/vonforst/evmap/api/UtilsTest.kt b/app/src/test/java/net/vonforst/evmap/api/UtilsTest.kt
deleted file mode 100644
index b9387c38..00000000
--- a/app/src/test/java/net/vonforst/evmap/api/UtilsTest.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.vonforst.evmap.api
-
-import org.junit.Assert.assertEquals
-import org.junit.Test
-
-
-class UtilsTest {
- @Test
- fun testDistanceBetween() {
- assertEquals(129412.71, distanceBetween(54.0, 9.0, 53.0, 8.0), 0.01)
- }
-}
\ No newline at end of file
diff --git a/app/src/test/java/net/vonforst/evmap/utils/LocationUtilsTest.kt b/app/src/test/java/net/vonforst/evmap/utils/LocationUtilsTest.kt
new file mode 100644
index 00000000..e9e6aecd
--- /dev/null
+++ b/app/src/test/java/net/vonforst/evmap/utils/LocationUtilsTest.kt
@@ -0,0 +1,12 @@
+package net.vonforst.evmap.utils
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+
+class LocationUtilsTest {
+ @Test
+ fun testDistanceBetween() {
+ assertEquals(129521.08, distanceBetween(54.0, 9.0, 53.0, 8.0), 0.01)
+ }
+}
\ No newline at end of file