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