mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-26 16:47:47 -05:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f6f09dc83 | ||
|
|
7f6d0c1391 | ||
|
|
96b60d0f49 | ||
|
|
2824f0b5c3 | ||
|
|
af0921ed20 | ||
|
|
a5b55479cb | ||
|
|
a93bacd9b3 | ||
|
|
9d7278e0e2 | ||
|
|
f6d9c615a0 | ||
|
|
a8ee3f5b7d | ||
|
|
826b4f89f1 | ||
|
|
5675d065e3 | ||
|
|
3e3531551d | ||
|
|
5d7d881729 | ||
|
|
23c73e3d7e | ||
|
|
7835aa8d78 | ||
|
|
f06b712090 | ||
|
|
317695954d | ||
|
|
24cfd1c10b | ||
|
|
775faa2f55 | ||
|
|
08bd2bdf5a | ||
|
|
90254915e3 | ||
|
|
b7f56ecff4 | ||
|
|
fa3910d3c8 | ||
|
|
4500c55560 | ||
|
|
a493e1a548 | ||
|
|
ddaab42e45 | ||
|
|
9f50341ab7 |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 31
|
||||
versionCode 64
|
||||
versionName "1.1.0"
|
||||
versionCode 66
|
||||
versionName "1.1.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -103,21 +103,25 @@ android {
|
||||
variant.resValue "string", "chargeprice_key", chargepriceKey
|
||||
}
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'NullSafeMutableLiveData'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation 'androidx.core:core-ktx:1.7.0'
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-alpha02'
|
||||
implementation "androidx.activity:activity-ktx:1.3.1"
|
||||
implementation "androidx.activity:activity-ktx:1.4.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.6"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.browser:browser:1.3.0'
|
||||
implementation 'androidx.browser:browser:1.4.0'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
|
||||
@@ -139,13 +143,14 @@ dependencies {
|
||||
implementation 'com.github.romandanylyk:PageIndicatorView:b1bad589b5'
|
||||
|
||||
// Android Auto
|
||||
googleImplementation 'androidx.car.app:app:1.1.0-beta01'
|
||||
googleImplementation 'androidx.car.app:app-projected:1.1.0-beta01'
|
||||
googleImplementation 'androidx.car.app:app:1.1.0-rc01'
|
||||
googleImplementation 'androidx.car.app:app-projected:1.1.0-rc01'
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = '8c0d46e7a6'
|
||||
def anyMapsVersion = '751daec281'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
googleImplementation 'com.google.android.gms:play-services-maps:18.0.0'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
|
||||
|
||||
// Google Places
|
||||
@@ -160,7 +165,7 @@ dependencies {
|
||||
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
|
||||
|
||||
// viewmodel library
|
||||
def lifecycle_version = "2.3.1"
|
||||
def lifecycle_version = "2.4.0"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
@@ -184,7 +189,7 @@ dependencies {
|
||||
// debug tools
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation "com.squareup.okhttp3:mockwebserver:4.9.0"
|
||||
//noinspection GradleDependency
|
||||
testImplementation 'org.json:json:20080701'
|
||||
|
||||
@@ -5,10 +5,18 @@ import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.gms.maps.MapsInitializer
|
||||
import com.google.android.libraries.places.api.Places
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.LocaleContextWrapper
|
||||
|
||||
fun init(context: Context) {
|
||||
Places.initialize(context, context.getString(R.string.google_maps_key));
|
||||
Places.initialize(context, context.getString(R.string.google_maps_key))
|
||||
|
||||
val localeContext = LocaleContextWrapper.wrap(
|
||||
context.applicationContext, PreferenceDataSource(context).language
|
||||
)
|
||||
MapsInitializer.initialize(localeContext, MapsInitializer.Renderer.LATEST, null)
|
||||
}
|
||||
|
||||
fun checkPlayServices(activity: Activity): Boolean {
|
||||
|
||||
@@ -14,14 +14,20 @@ import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import moe.banana.jsonapi2.HasMany
|
||||
import moe.banana.jsonapi2.HasOne
|
||||
import moe.banana.jsonapi2.JsonBuffer
|
||||
import moe.banana.jsonapi2.ResourceIdentifier
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.chargeprice.*
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.ui.currency
|
||||
import java.io.IOException
|
||||
|
||||
class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(ctx) {
|
||||
private val prefs = PreferenceDataSource(ctx)
|
||||
@@ -164,94 +170,120 @@ class ChargepriceScreen(ctx: CarContext, val charger: ChargeLocation) : Screen(c
|
||||
val manufacturer = model?.manufacturer?.value
|
||||
val modelName = model?.name?.value
|
||||
lifecycleScope.launch {
|
||||
var vehicles = api.getVehicles().filter {
|
||||
it.id in prefs.chargepriceMyVehicles
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
|
||||
invalidate()
|
||||
return@launch
|
||||
} else if (vehicles.size > 1) {
|
||||
if (manufacturer != null && modelName != null) {
|
||||
vehicles = vehicles.filter {
|
||||
it.brand == manufacturer && it.name.startsWith(modelName)
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
errorMessage = carContext.getString(
|
||||
R.string.auto_chargeprice_vehicle_unknown,
|
||||
manufacturer,
|
||||
modelName
|
||||
)
|
||||
invalidate()
|
||||
return@launch
|
||||
} else if (vehicles.size > 1) {
|
||||
errorMessage = carContext.getString(
|
||||
R.string.auto_chargeprice_vehicle_ambiguous,
|
||||
manufacturer,
|
||||
modelName
|
||||
)
|
||||
try {
|
||||
var vehicles = api.getVehicles().filter {
|
||||
it.id in prefs.chargepriceMyVehicles
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
errorMessage = carContext.getString(R.string.chargeprice_select_car_first)
|
||||
invalidate()
|
||||
return@launch
|
||||
} else if (vehicles.size > 1) {
|
||||
if (manufacturer != null && modelName != null) {
|
||||
vehicles = vehicles.filter {
|
||||
it.brand == manufacturer && it.name.startsWith(modelName)
|
||||
}
|
||||
if (vehicles.isEmpty()) {
|
||||
errorMessage = carContext.getString(
|
||||
R.string.auto_chargeprice_vehicle_unknown,
|
||||
manufacturer,
|
||||
modelName
|
||||
)
|
||||
invalidate()
|
||||
return@launch
|
||||
} else if (vehicles.size > 1) {
|
||||
errorMessage = carContext.getString(
|
||||
R.string.auto_chargeprice_vehicle_ambiguous,
|
||||
manufacturer,
|
||||
modelName
|
||||
)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
} else {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
} else {
|
||||
}
|
||||
val car = vehicles[0]
|
||||
|
||||
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
|
||||
val result = api.getChargePrices(ChargepriceRequest().apply {
|
||||
this.dataAdapter = dataAdapter
|
||||
station = cpStation
|
||||
vehicle = HasOne(car)
|
||||
tariffs = if (!prefs.chargepriceMyTariffsAll) {
|
||||
val myTariffs = prefs.chargepriceMyTariffs ?: emptySet()
|
||||
HasMany<ChargepriceTariff>(*myTariffs.map {
|
||||
ResourceIdentifier(
|
||||
"tariff",
|
||||
it
|
||||
)
|
||||
}.toTypedArray()).apply {
|
||||
meta = JsonBuffer.create(
|
||||
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
|
||||
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
|
||||
)
|
||||
}
|
||||
} else null
|
||||
options = ChargepriceOptions(
|
||||
batteryRange = batteryRange,
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency
|
||||
)
|
||||
}, ChargepriceApi.getChargepriceLanguage())
|
||||
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
|
||||
// choose the highest power chargepoint compatible with the car
|
||||
val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
|
||||
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
|
||||
}.maxByOrNull { it.power }
|
||||
if (chargepoint == null) {
|
||||
errorMessage =
|
||||
carContext.getString(R.string.auto_chargeprice_vehicle_unavailable)
|
||||
carContext.getString(R.string.chargeprice_no_compatible_connectors)
|
||||
invalidate()
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
val car = vehicles[0]
|
||||
meta =
|
||||
(result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
|
||||
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
|
||||
}.maxByOrNull {
|
||||
it.power
|
||||
}
|
||||
|
||||
val cpStation = ChargepriceStation.fromEvmap(charger, car.compatibleEvmapConnectors)
|
||||
val result = api.getChargePrices(ChargepriceRequest().apply {
|
||||
this.dataAdapter = dataAdapter
|
||||
station = cpStation
|
||||
vehicle = HasOne(car)
|
||||
options = ChargepriceOptions(
|
||||
batteryRange = batteryRange,
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
maxMonthlyFees = if (prefs.chargepriceNoBaseFee) 0.0 else null,
|
||||
currency = prefs.chargepriceCurrency
|
||||
)
|
||||
}, ChargepriceApi.getChargepriceLanguage())
|
||||
|
||||
val myTariffs = prefs.chargepriceMyTariffs
|
||||
|
||||
// choose the highest power chargepoint compatible with the car
|
||||
val chargepoint = cpStation.chargePoints.filterIndexed { i, cp ->
|
||||
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
|
||||
}.maxByOrNull { it.power }
|
||||
if (chargepoint == null) {
|
||||
errorMessage = carContext.getString(R.string.chargeprice_no_compatible_connectors)
|
||||
prices = result.map { cp ->
|
||||
val filteredPrices =
|
||||
cp.chargepointPrices.filter {
|
||||
it.plug == chargepoint.plug && it.power == chargepoint.power
|
||||
}
|
||||
if (filteredPrices.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
cp.clone().apply {
|
||||
chargepointPrices = filteredPrices
|
||||
}
|
||||
}
|
||||
}.filterNotNull()
|
||||
.sortedBy { it.chargepointPrices.first().price }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
myTariffs != null && it.tariff?.get()?.id in myTariffs
|
||||
}
|
||||
invalidate()
|
||||
return@launch
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.chargeprice_connection_error,
|
||||
CarToast.LENGTH_LONG
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
meta =
|
||||
(result.meta.get<ChargepriceMeta>(ChargepriceApi.moshi.adapter(ChargepriceMeta::class.java)) as ChargepriceMeta).chargePoints.filterIndexed { i, cp ->
|
||||
charger.chargepointsMerged[i].type in car.compatibleEvmapConnectors
|
||||
}.maxByOrNull {
|
||||
it.power
|
||||
}
|
||||
|
||||
prices = result.map { cp ->
|
||||
val filteredPrices =
|
||||
cp.chargepointPrices.filter {
|
||||
it.plug == chargepoint.plug && it.power == chargepoint.power
|
||||
}
|
||||
if (filteredPrices.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
cp.clone().apply {
|
||||
chargepointPrices = filteredPrices
|
||||
}
|
||||
}
|
||||
}.filterNotNull()
|
||||
.sortedBy { it.chargepointPrices.first().price }
|
||||
.sortedByDescending {
|
||||
prefs.chargepriceMyTariffsAll ||
|
||||
myTariffs != null && it.tariff?.get()?.id in myTariffs
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.Location
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
@@ -7,10 +8,14 @@ import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.CarHardwareManager
|
||||
import androidx.car.app.hardware.info.EnergyLevel
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.OnLifecycleEvent
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
@@ -34,6 +39,7 @@ import net.vonforst.evmap.viewmodel.getFilters
|
||||
import net.vonforst.evmap.viewmodel.getReferenceData
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.collections.set
|
||||
import kotlin.math.roundToInt
|
||||
@@ -52,7 +58,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
private val maxNumUpdates = 1
|
||||
|
||||
private var location: Location? = null
|
||||
private var lastUpdateLocation: Location? = null
|
||||
private var lastChargerUpdateLocation: Location? = null
|
||||
private var lastDistanceUpdateTime: Instant? = null
|
||||
private var chargers: List<ChargeLocation>? = null
|
||||
private var prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
@@ -60,7 +67,8 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
private val searchRadius = 5 // kilometers
|
||||
private val updateThreshold = 2000 // meters
|
||||
private val chargerUpdateThreshold = 2000 // meters
|
||||
private val distanceUpdateThreshold = Duration.ofSeconds(15)
|
||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus>> =
|
||||
HashMap()
|
||||
@@ -77,6 +85,9 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
private val filters = api.getFilters(referenceData, carContext.stringProvider())
|
||||
private val filtersWithValue = filtersWithValue(filters, filterValues)
|
||||
|
||||
private val hardwareMan = ctx.getCarService(CarContext.HARDWARE_SERVICE) as CarHardwareManager
|
||||
private var energyLevel: EnergyLevel? = null
|
||||
|
||||
init {
|
||||
filtersWithValue.observe(this) {
|
||||
loadChargers()
|
||||
@@ -186,13 +197,18 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
|
||||
// distance
|
||||
location?.let {
|
||||
val distance = distanceBetween(
|
||||
val distanceMeters = distanceBetween(
|
||||
it.latitude, it.longitude,
|
||||
charger.coordinates.lat, charger.coordinates.lng
|
||||
) / 1000
|
||||
)
|
||||
text.append(
|
||||
"distance",
|
||||
DistanceSpan.create(Distance.create(distance, Distance.UNIT_KILOMETERS)),
|
||||
DistanceSpan.create(
|
||||
roundValueToDistance(
|
||||
distanceMeters,
|
||||
energyLevel?.distanceDisplayUnit?.value
|
||||
)
|
||||
),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
@@ -240,12 +256,19 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
return
|
||||
}
|
||||
|
||||
invalidate()
|
||||
|
||||
if (lastUpdateLocation == null ||
|
||||
location.distanceTo(lastUpdateLocation) > updateThreshold
|
||||
val now = Instant.now()
|
||||
if (lastDistanceUpdateTime == null ||
|
||||
Duration.between(lastDistanceUpdateTime, now) > distanceUpdateThreshold
|
||||
) {
|
||||
lastUpdateLocation = location
|
||||
lastDistanceUpdateTime = now
|
||||
// update displayed distances
|
||||
invalidate()
|
||||
}
|
||||
|
||||
if (lastChargerUpdateLocation == null ||
|
||||
location.distanceTo(lastChargerUpdateLocation) > chargerUpdateThreshold
|
||||
) {
|
||||
lastChargerUpdateLocation = location
|
||||
// update displayed chargers
|
||||
loadChargers()
|
||||
}
|
||||
@@ -321,6 +344,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
}?.awaitAll()
|
||||
|
||||
updateCoroutine = null
|
||||
lastDistanceUpdateTime = Instant.now()
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -330,4 +354,30 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEnergyLevelUpdated(energyLevel: EnergyLevel) {
|
||||
this.energyLevel = energyLevel
|
||||
invalidate()
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
private fun setupListeners() {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
carContext,
|
||||
"com.google.android.gms.permission.CAR_FUEL"
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
return
|
||||
|
||||
println("Setting up energy level listener")
|
||||
|
||||
val exec = ContextCompat.getMainExecutor(carContext)
|
||||
hardwareMan.carInfo.addEnergyLevelListener(exec, ::onEnergyLevelUpdated)
|
||||
}
|
||||
|
||||
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
|
||||
private fun removeListeners() {
|
||||
println("Removing energy level listener")
|
||||
hardwareMan.carInfo.removeEnergyLevelListener(::onEnergyLevelUpdated)
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,12 @@ import androidx.car.app.constraints.ConstraintManager
|
||||
import androidx.car.app.hardware.common.CarUnit
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.Distance
|
||||
import androidx.car.app.versioning.CarAppApiLevels
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
|
||||
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
|
||||
@@ -34,14 +36,22 @@ val CarContext.constraintManager
|
||||
fun Bitmap.asCarIcon(): CarIcon = CarIcon.Builder(IconCompat.createWithBitmap(this)).build()
|
||||
|
||||
private const val kmPerMile = 1.609344
|
||||
private const val ftPerMile = 5280
|
||||
private const val ydPerMile = 1760
|
||||
|
||||
fun getDefaultDistanceUnit(): Int {
|
||||
return when (Locale.getDefault().country) {
|
||||
"US", "GB", "MM", "LR" -> CarUnit.MILE
|
||||
else -> CarUnit.KILOMETER
|
||||
return if (usesImperialUnits(Locale.getDefault())) {
|
||||
CarUnit.MILE
|
||||
} else {
|
||||
CarUnit.KILOMETER
|
||||
}
|
||||
}
|
||||
|
||||
fun usesImperialUnits(locale: Locale): Boolean {
|
||||
return locale.country in listOf("US", "GB", "MM", "LR")
|
||||
|| locale.country == "" && locale.language == "en"
|
||||
}
|
||||
|
||||
fun getDefaultSpeedUnit(): Int {
|
||||
return when (Locale.getDefault().country) {
|
||||
"US", "GB", "MM", "LR" -> CarUnit.MILES_PER_HOUR
|
||||
@@ -72,6 +82,52 @@ fun formatCarUnitSpeed(value: Float?, unit: Int?): String {
|
||||
}
|
||||
}
|
||||
|
||||
fun roundValueToDistance(value: Double, unit: Int? = null): Distance {
|
||||
// value is in meters
|
||||
when (unit ?: getDefaultDistanceUnit()) {
|
||||
CarUnit.MILE -> {
|
||||
// imperial system
|
||||
val miles = value / 1000 / kmPerMile
|
||||
val yards = miles * ydPerMile
|
||||
val feet = miles * ftPerMile
|
||||
|
||||
return when (miles) {
|
||||
in 0.0..0.1 -> if (Locale.getDefault().country == "UK") {
|
||||
Distance.create(roundToMultipleOf(yards, 10.0), Distance.UNIT_YARDS)
|
||||
} else {
|
||||
Distance.create(roundToMultipleOf(feet, 10.0), Distance.UNIT_FEET)
|
||||
}
|
||||
in 0.1..10.0 -> Distance.create(
|
||||
roundToMultipleOf(miles, 0.1),
|
||||
Distance.UNIT_MILES_P1
|
||||
)
|
||||
else -> Distance.create(roundToMultipleOf(miles, 1.0), Distance.UNIT_MILES)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// metric system
|
||||
return when (value) {
|
||||
in 0.0..999.0 -> Distance.create(
|
||||
roundToMultipleOf(value, 10.0),
|
||||
Distance.UNIT_METERS
|
||||
)
|
||||
in 1000.0..10000.0 -> Distance.create(
|
||||
roundToMultipleOf(value / 1000, 0.1),
|
||||
Distance.UNIT_KILOMETERS_P1
|
||||
)
|
||||
else -> Distance.create(
|
||||
roundToMultipleOf(value / 1000, 1.0),
|
||||
Distance.UNIT_KILOMETERS
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun roundToMultipleOf(num: Double, step: Double): Double {
|
||||
return (num / step).roundToInt() * step
|
||||
}
|
||||
|
||||
fun getAndroidAutoVersion(ctx: Context): List<String> {
|
||||
val info = ctx.packageManager.getPackageInfo("com.google.android.projection.gearhead", 0)
|
||||
return info.versionName.split(".")
|
||||
|
||||
@@ -84,10 +84,10 @@ fun buildDetails(
|
||||
loc.openinghours.getStatusText(ctx)
|
||||
else
|
||||
loc.openinghours.description ?: "",
|
||||
if (loc.openinghours.days != null) loc.openinghours.description else null,
|
||||
if (loc.openinghours.days != null || loc.openinghours.twentyfourSeven) loc.openinghours.description else null,
|
||||
hoursDays = loc.openinghours.days
|
||||
) else null,
|
||||
if (loc.cost != null) DetailsAdapter.Detail(
|
||||
if (loc.cost != null && !loc.cost.isEmpty) DetailsAdapter.Detail(
|
||||
R.drawable.ic_cost,
|
||||
R.string.cost,
|
||||
loc.cost.getStatusText(ctx),
|
||||
|
||||
@@ -65,10 +65,20 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
connectors: Map<Long, Pair<Double, String>>,
|
||||
chargepoints: List<Chargepoint>
|
||||
): Map<Chargepoint, Set<Long>> {
|
||||
var chargepoints = chargepoints
|
||||
|
||||
// iterate over each connector type
|
||||
val types = connectors.map { it.value.second }.distinct().toSet()
|
||||
val equivalentTypes = types.map { equivalentPlugTypes(it).plus(it) }.cartesianProduct()
|
||||
val geTypes = chargepoints.map { it.type }.distinct().toSet()
|
||||
var geTypes = chargepoints.map { it.type }.distinct().toSet()
|
||||
if (!equivalentTypes.any { it == geTypes } && geTypes.size > 1 && geTypes.contains(
|
||||
Chargepoint.SCHUKO
|
||||
)) {
|
||||
// If charger has household plugs and other plugs, try removing the household plugs
|
||||
// (common e.g. in Hamburg -> 2x Type 2 + 2x Schuko, but NM only lists Type 2)
|
||||
geTypes = geTypes.filter { it != Chargepoint.SCHUKO }.toSet()
|
||||
chargepoints = chargepoints.filter { it.type != Chargepoint.SCHUKO }
|
||||
}
|
||||
if (!equivalentTypes.any { it == geTypes }) throw AvailabilityDetectorException("chargepoints do not match")
|
||||
return types.flatMap { type ->
|
||||
// find connectors of this type
|
||||
@@ -92,7 +102,7 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
chargepoint to ids
|
||||
}
|
||||
} else if (powers.size == 1 && gePowers.size == 2
|
||||
&& chargepoints.sumBy { it.count } == connsOfType.size
|
||||
&& chargepoints.sumOf { it.count } == connsOfType.size
|
||||
) {
|
||||
// special case: dual charger(s) with load balancing
|
||||
// GoingElectric shows 2 different powers, NewMotion just one
|
||||
|
||||
@@ -205,6 +205,9 @@ class ChargePrice : Resource(), Equatable, Cloneable {
|
||||
@field:Json(name = "charge_point_prices")
|
||||
lateinit var chargepointPrices: List<ChargepointPrice>
|
||||
|
||||
@field:Json(name = "branding")
|
||||
var branding: ChargepriceBranding? = null
|
||||
|
||||
var tariff: HasOne<ChargepriceTariff>? = null
|
||||
|
||||
|
||||
@@ -238,6 +241,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
|
||||
if (startTime != other.startTime) return false
|
||||
if (tags != other.tags) return false
|
||||
if (chargepointPrices != other.chargepointPrices) return false
|
||||
if (branding != other.branding) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -256,6 +260,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
|
||||
result = 31 * result + startTime
|
||||
result = 31 * result + tags.hashCode()
|
||||
result = 31 * result + chargepointPrices.hashCode()
|
||||
result = 31 * result + branding.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -274,6 +279,7 @@ class ChargePrice : Resource(), Equatable, Cloneable {
|
||||
totalMonthlyFee = this@ChargePrice.totalMonthlyFee
|
||||
url = this@ChargePrice.url
|
||||
tariff = this@ChargePrice.tariff
|
||||
branding = this@ChargePrice.branding
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -328,6 +334,12 @@ data class ChargepointPrice(
|
||||
}
|
||||
}
|
||||
|
||||
data class ChargepriceBranding(
|
||||
@Json(name = "background_color") val backgroundColor: String,
|
||||
@Json(name = "text_color") val textColor: String,
|
||||
@Json(name = "logo_url") val logoUrl: String
|
||||
)
|
||||
|
||||
data class PriceDistribution(val kwh: Double?, val session: Double?, val minute: Double?) {
|
||||
val isOnlyKwh =
|
||||
kwh != null && kwh > 0 && (session == null || session == 0.0) && (minute == null || minute == 0.0)
|
||||
@@ -339,6 +351,19 @@ data class ChargepriceMeta(
|
||||
@Json(name = "charge_points") val chargePoints: List<ChargepriceChargepointMeta>
|
||||
)
|
||||
|
||||
enum class ChargepriceInclude {
|
||||
@Json(name = "filter")
|
||||
FILTER,
|
||||
@Json(name = "always")
|
||||
ALWAYS,
|
||||
@Json(name = "exclusive")
|
||||
EXCLUSIVE
|
||||
}
|
||||
|
||||
data class ChargepriceRequestTariffMeta(
|
||||
val include: ChargepriceInclude
|
||||
)
|
||||
|
||||
data class ChargepriceChargepointMeta(
|
||||
val power: Double,
|
||||
val plug: String,
|
||||
|
||||
@@ -87,7 +87,14 @@ data class GECost(
|
||||
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String?,
|
||||
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String?
|
||||
) {
|
||||
fun convert() = Cost(freecharging, freeparking, descriptionShort, descriptionLong)
|
||||
fun convert() = Cost(
|
||||
// In GE, freecharging = false can either mean "paid charging" or "no information
|
||||
// available", only freecharging = true provides useful information. Therefore convert
|
||||
// false to null. Same for freeparking.
|
||||
if (freecharging) freecharging else null,
|
||||
if (freeparking) freeparking else null,
|
||||
descriptionShort, descriptionLong
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@@ -126,7 +133,7 @@ data class GEHours(
|
||||
val start: LocalTime?,
|
||||
val end: LocalTime?
|
||||
) {
|
||||
fun convert() = Hours(start, end)
|
||||
fun convert() = if (start != null && end != null) Hours(start, end) else null
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package net.vonforst.evmap.autocomplete
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
interface AutocompleteProvider {
|
||||
val id: String
|
||||
@@ -183,4 +185,5 @@ enum class AutocompletePlaceType {
|
||||
}
|
||||
}
|
||||
|
||||
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?)
|
||||
@Parcelize
|
||||
data class PlaceWithBounds(val latLng: LatLng, val viewport: LatLngBounds?) : Parcelable
|
||||
@@ -175,6 +175,10 @@ class ChargepriceFragment : DialogFragment() {
|
||||
dismiss()
|
||||
true
|
||||
}
|
||||
R.id.menu_help -> {
|
||||
(activity as? MapsActivity)?.openUrl(getString(R.string.chargeprice_faq_link))
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -910,6 +910,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.mapPosition.value = MapPosition(
|
||||
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
|
||||
)
|
||||
|
||||
if (vm.searchResult.value != null) {
|
||||
// show search result (after configuration change)
|
||||
vm.searchResult.postValue(vm.searchResult.value)
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(anyOf = [ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION])
|
||||
|
||||
@@ -126,6 +126,9 @@ data class Cost(
|
||||
val descriptionShort: String? = null,
|
||||
val descriptionLong: String? = null
|
||||
) : Parcelable {
|
||||
val isEmpty: Boolean
|
||||
get() = descriptionLong == null && descriptionShort == null && freecharging == null && freeparking == null
|
||||
|
||||
fun getStatusText(ctx: Context, emoji: Boolean = false): CharSequence {
|
||||
if (freecharging != null && freeparking != null) {
|
||||
val charging =
|
||||
@@ -137,6 +140,22 @@ data class Cost(
|
||||
} else {
|
||||
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail, charging, parking), 0)
|
||||
}
|
||||
} else if (freecharging != null) {
|
||||
val charging =
|
||||
if (freecharging) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
return if (emoji) {
|
||||
"⚡ $charging"
|
||||
} else {
|
||||
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail_charging, charging), 0)
|
||||
}
|
||||
} else if (freeparking != null) {
|
||||
val parking =
|
||||
if (freeparking) ctx.getString(R.string.free) else ctx.getString(R.string.paid)
|
||||
return if (emoji) {
|
||||
"⚡ $parking"
|
||||
} else {
|
||||
HtmlCompat.fromHtml(ctx.getString(R.string.cost_detail_parking, parking), 0)
|
||||
}
|
||||
} else if (descriptionShort != null) {
|
||||
return descriptionShort
|
||||
} else if (descriptionLong != null) {
|
||||
@@ -162,9 +181,7 @@ data class OpeningHours(
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.open_247), 0)
|
||||
} else if (days != null) {
|
||||
val hours = days.getHoursForDate(LocalDate.now())
|
||||
if (hours.start == null || hours.end == null) {
|
||||
return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
|
||||
}
|
||||
?: return HtmlCompat.fromHtml(ctx.getString(R.string.closed), 0)
|
||||
|
||||
val now = LocalTime.now()
|
||||
if (hours.start.isBefore(now) && hours.end.isAfter(now)) {
|
||||
@@ -192,21 +209,21 @@ data class OpeningHours(
|
||||
|
||||
@Parcelize
|
||||
data class OpeningHoursDays(
|
||||
@Embedded(prefix = "mo") val monday: Hours,
|
||||
@Embedded(prefix = "tu") val tuesday: Hours,
|
||||
@Embedded(prefix = "we") val wednesday: Hours,
|
||||
@Embedded(prefix = "th") val thursday: Hours,
|
||||
@Embedded(prefix = "fr") val friday: Hours,
|
||||
@Embedded(prefix = "sa") val saturday: Hours,
|
||||
@Embedded(prefix = "su") val sunday: Hours,
|
||||
@Embedded(prefix = "ho") val holiday: Hours
|
||||
@Embedded(prefix = "mo") val monday: Hours?,
|
||||
@Embedded(prefix = "tu") val tuesday: Hours?,
|
||||
@Embedded(prefix = "we") val wednesday: Hours?,
|
||||
@Embedded(prefix = "th") val thursday: Hours?,
|
||||
@Embedded(prefix = "fr") val friday: Hours?,
|
||||
@Embedded(prefix = "sa") val saturday: Hours?,
|
||||
@Embedded(prefix = "su") val sunday: Hours?,
|
||||
@Embedded(prefix = "ho") val holiday: Hours?
|
||||
) : Parcelable {
|
||||
fun getHoursForDate(date: LocalDate): Hours {
|
||||
fun getHoursForDate(date: LocalDate): Hours? {
|
||||
// TODO: check for holidays
|
||||
return getHoursForDayOfWeek(date.dayOfWeek)
|
||||
}
|
||||
|
||||
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours {
|
||||
fun getHoursForDayOfWeek(dayOfWeek: DayOfWeek?): Hours? {
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
return when (dayOfWeek) {
|
||||
DayOfWeek.MONDAY -> monday
|
||||
@@ -223,16 +240,12 @@ data class OpeningHoursDays(
|
||||
|
||||
@Parcelize
|
||||
data class Hours(
|
||||
val start: LocalTime?,
|
||||
val end: LocalTime?
|
||||
val start: LocalTime,
|
||||
val end: LocalTime
|
||||
) : Parcelable {
|
||||
override fun toString(): String {
|
||||
if (start != null && end != null) {
|
||||
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
return "${start.format(fmt)} - ${end.format(fmt)}"
|
||||
} else {
|
||||
return "closed"
|
||||
}
|
||||
val fmt = DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT)
|
||||
return "${start.format(fmt)} - ${end.format(fmt)}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,10 @@ package net.vonforst.evmap.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.text.SpannableString
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
@@ -18,14 +22,12 @@ import androidx.databinding.InverseBindingListener
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import coil.load
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.slider.RangeSlider
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.iconForPlugType
|
||||
import net.vonforst.evmap.kmPerMile
|
||||
import net.vonforst.evmap.meterPerFt
|
||||
import net.vonforst.evmap.shouldUseImperialUnits
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
@@ -344,18 +346,68 @@ fun setImageTintList(view: ImageView, @ColorInt color: Int) {
|
||||
view.imageTintList = ColorStateList.valueOf(color)
|
||||
}
|
||||
|
||||
@BindingAdapter("myTariffsBackground")
|
||||
fun myTariffsBackground(view: View, myTariff: Boolean) {
|
||||
if (myTariff) {
|
||||
view.background = ContextCompat.getDrawable(view.context, R.drawable.my_tariff_background)
|
||||
} else {
|
||||
view.context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
|
||||
view.background = it.getDrawable(0)
|
||||
fun tariffBackground(context: Context, myTariff: Boolean, brandingColor: String?): Drawable? {
|
||||
when {
|
||||
myTariff -> {
|
||||
return ContextCompat.getDrawable(context, R.drawable.my_tariff_background)
|
||||
}
|
||||
brandingColor != null -> {
|
||||
val drawable = ContextCompat.getDrawable(context, R.drawable.branded_tariff_background)
|
||||
val color = colorToTransparent(Color.parseColor(brandingColor))
|
||||
(drawable as LayerDrawable).setDrawableByLayerId(
|
||||
R.id.background, ColorDrawable(
|
||||
color
|
||||
)
|
||||
)
|
||||
return drawable
|
||||
}
|
||||
else -> {
|
||||
context.obtainStyledAttributes(intArrayOf(R.attr.selectableItemBackground)).use {
|
||||
return it.getDrawable(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isDarkMode(context: Context) = context.isDarkMode()
|
||||
|
||||
/**
|
||||
* Converts an opaque color to a transparent color, assuming it was on a white background
|
||||
* with a certain opacity targetAlpha.
|
||||
*/
|
||||
private fun colorToTransparent(color: Int, targetAlpha: Float = 31f / 255): Int {
|
||||
if (Color.alpha(color) != 255) return color
|
||||
|
||||
val red = Color.red(color)
|
||||
val green = Color.green(color)
|
||||
val blue = Color.blue(color)
|
||||
|
||||
val newRed = ((red - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
|
||||
val newGreen = ((green - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
|
||||
val newBlue = ((blue - (1 - targetAlpha) * 255) / targetAlpha).roundToInt()
|
||||
|
||||
return Color.argb((targetAlpha * 255).roundToInt(), newRed, newGreen, newBlue)
|
||||
}
|
||||
|
||||
@BindingAdapter("imageUrl")
|
||||
fun loadImage(view: ImageView, url: String?) {
|
||||
if (url != null) {
|
||||
view.load(url)
|
||||
} else {
|
||||
view.setImageDrawable(null)
|
||||
}
|
||||
}
|
||||
|
||||
@BindingAdapter("tooltipTextCompat")
|
||||
fun setTooltipTextCompat(view: View, text: String) {
|
||||
TooltipCompat.setTooltipText(view, text)
|
||||
}
|
||||
|
||||
@BindingAdapter("tintNullable")
|
||||
fun setImageTint(view: ImageView, @ColorInt tint: Int?) {
|
||||
if (tint != null) {
|
||||
view.imageTintList = ColorStateList.valueOf(tint)
|
||||
} else {
|
||||
view.imageTintList = null
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import android.app.Application
|
||||
import androidx.lifecycle.*
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.banana.jsonapi2.HasMany
|
||||
import moe.banana.jsonapi2.HasOne
|
||||
import moe.banana.jsonapi2.JsonBuffer
|
||||
import moe.banana.jsonapi2.ResourceIdentifier
|
||||
import net.vonforst.evmap.api.chargeprice.*
|
||||
import net.vonforst.evmap.api.equivalentPlugTypes
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
@@ -100,7 +103,8 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
dataSource,
|
||||
batteryRange,
|
||||
batteryRangeSliderDragging,
|
||||
vehicleCompatibleConnectors
|
||||
vehicleCompatibleConnectors,
|
||||
myTariffs, myTariffsAll
|
||||
).forEach {
|
||||
addSource(it) {
|
||||
if (!batteryRangeSliderDragging.value!!) loadPrices()
|
||||
@@ -208,7 +212,9 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
val car = vehicle.value
|
||||
val compatibleConnectors = vehicleCompatibleConnectors.value
|
||||
val dataSource = dataSource.value
|
||||
if (charger == null || car == null || compatibleConnectors == null || dataSource == null) {
|
||||
val myTariffs = myTariffs.value
|
||||
val myTariffsAll = myTariffsAll.value
|
||||
if (charger == null || car == null || compatibleConnectors == null || dataSource == null || myTariffs == null || myTariffsAll == null) {
|
||||
chargePrices.value = Resource.error(null, null)
|
||||
return
|
||||
}
|
||||
@@ -222,6 +228,19 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
dataAdapter = dataSource
|
||||
station = cpStation
|
||||
vehicle = HasOne(car)
|
||||
tariffs = if (!myTariffsAll) {
|
||||
HasMany<ChargepriceTariff>(*myTariffs.map {
|
||||
ResourceIdentifier(
|
||||
"tariff",
|
||||
it
|
||||
)
|
||||
}.toTypedArray()).apply {
|
||||
meta = JsonBuffer.create(
|
||||
ChargepriceApi.moshi.adapter(ChargepriceRequestTariffMeta::class.java),
|
||||
ChargepriceRequestTariffMeta(ChargepriceInclude.ALWAYS)
|
||||
)
|
||||
}
|
||||
} else null
|
||||
options = ChargepriceOptions(
|
||||
batteryRange = batteryRange.value!!.map { it.toDouble() },
|
||||
providerCustomerTariffs = prefs.chargepriceShowProviderCustomerTariffs,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.*
|
||||
import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
@@ -27,7 +29,8 @@ import net.vonforst.evmap.ui.cluster
|
||||
import net.vonforst.evmap.utils.distanceBetween
|
||||
import java.io.IOException
|
||||
|
||||
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
|
||||
@Parcelize
|
||||
data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
|
||||
|
||||
internal fun getClusterDistance(zoom: Float): Int? {
|
||||
return when (zoom) {
|
||||
@@ -39,7 +42,8 @@ internal fun getClusterDistance(zoom: Float): Int? {
|
||||
}
|
||||
}
|
||||
|
||||
class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
class MapViewModel(application: Application, private val state: SavedStateHandle) :
|
||||
AndroidViewModel(application) {
|
||||
val apiType: Class<ChargepointApi<ReferenceData>>
|
||||
get() = api.javaClass
|
||||
val apiName: String
|
||||
@@ -50,11 +54,11 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
|
||||
|
||||
val bottomSheetState: MutableLiveData<Int> by lazy {
|
||||
MutableLiveData<Int>()
|
||||
state.getLiveData("bottomSheetState")
|
||||
}
|
||||
|
||||
val mapPosition: MutableLiveData<MapPosition> by lazy {
|
||||
MutableLiveData<MapPosition>()
|
||||
state.getLiveData("mapPosition")
|
||||
}
|
||||
val filterStatus: MutableLiveData<Long> by lazy {
|
||||
MutableLiveData<Long>().apply {
|
||||
@@ -125,7 +129,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
val chargerSparse: MutableLiveData<ChargeLocation> by lazy {
|
||||
MutableLiveData<ChargeLocation>()
|
||||
state.getLiveData("chargerSparse")
|
||||
}
|
||||
val chargerDetails: MediatorLiveData<Resource<ChargeLocation>> by lazy {
|
||||
MediatorLiveData<Resource<ChargeLocation>>().apply {
|
||||
@@ -223,7 +227,7 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
val searchResult: MutableLiveData<PlaceWithBounds> by lazy {
|
||||
MutableLiveData<PlaceWithBounds>()
|
||||
state.getLiveData("searchResult")
|
||||
}
|
||||
|
||||
val mapType: MutableLiveData<AnyMap.Type> by lazy {
|
||||
@@ -297,9 +301,6 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
viewModelScope
|
||||
) { data: Triple<MapPosition, FilterValues, ReferenceData> ->
|
||||
chargepoints.value = Resource.loading(chargepoints.value?.data)
|
||||
filteredConnectors.value = null
|
||||
filteredMinPower.value = null
|
||||
filteredChargeCards.value = null
|
||||
|
||||
val mapPosition = data.first
|
||||
val filters = data.second
|
||||
@@ -320,17 +321,13 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
clusterDistance?.let {
|
||||
chargers = cluster(chargers, mapPosition.zoom, clusterDistance)
|
||||
}
|
||||
filteredConnectors.value = null
|
||||
filteredMinPower.value = null
|
||||
filteredChargeCards.value = null
|
||||
chargepoints.value = Resource.success(chargers)
|
||||
return@throttleLatest
|
||||
}
|
||||
|
||||
var result = api.getChargepoints(refData, mapPosition.bounds, mapPosition.zoom, filters)
|
||||
if (result.status == Status.ERROR && result.data == null) {
|
||||
// keep old results if new data could not be loaded
|
||||
result = Resource.error(result.message, chargepoints.value?.data)
|
||||
}
|
||||
chargepoints.value = result
|
||||
|
||||
if (api is GoingElectricApiWrapper) {
|
||||
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
|
||||
filteredChargeCards.value =
|
||||
@@ -353,7 +350,19 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
)
|
||||
}.toSet()
|
||||
filteredMinPower.value = filters.getSliderValue("minPower")
|
||||
} else {
|
||||
filteredConnectors.value = null
|
||||
filteredMinPower.value = null
|
||||
filteredChargeCards.value = null
|
||||
}
|
||||
|
||||
var result = api.getChargepoints(refData, mapPosition.bounds, mapPosition.zoom, filters)
|
||||
if (result.status == Status.ERROR && result.data == null) {
|
||||
// keep old results if new data could not be loaded
|
||||
result = Resource.error(result.message, chargepoints.value?.data)
|
||||
}
|
||||
|
||||
chargepoints.value = result
|
||||
}
|
||||
|
||||
private suspend fun loadAvailability(charger: ChargeLocation) {
|
||||
|
||||
7
app/src/main/res/drawable/branded_tariff_background.xml
Normal file
7
app/src/main/res/drawable/branded_tariff_background.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/background"
|
||||
android:drawable="@color/chip_background" />
|
||||
<item android:drawable="?selectableItemBackground" />
|
||||
</layer-list>
|
||||
11
app/src/main/res/drawable/ic_help.xml
Normal file
11
app/src/main/res/drawable/ic_help.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11,18h2v-2h-2v2zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM12,6c-2.21,0 -4,1.79 -4,4h2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2c0,2 -3,1.75 -3,5h2c0,-2.25 3,-2.5 3,-5 0,-2.21 -1.79,-4 -4,-4z" />
|
||||
</vector>
|
||||
@@ -317,7 +317,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/go_to_chargeprice"
|
||||
app:goneUnless="@{charger.data != null && ChargepriceApi.isCountrySupported(charger.data.chargepriceData.country, charger.data.dataSource)}"
|
||||
app:goneUnless="@{charger.data != null && charger.data.chargepriceData != null && charger.data.chargepriceData.country != null && ChargepriceApi.isCountrySupported(charger.data.chargepriceData.country, charger.data.dataSource)}"
|
||||
app:icon="@drawable/ic_chargeprice"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline2"
|
||||
app:layout_constraintStart_toStartOf="@+id/guideline"
|
||||
|
||||
@@ -37,17 +37,16 @@
|
||||
android:paddingTop="8dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="8dp"
|
||||
app:myTariffsBackground="@{!myTariffsAll && myTariffs.contains(item.tariff.get().id)}">
|
||||
android:background="@{BindingAdaptersKt.tariffBackground(context,!myTariffsAll && myTariffs.contains(item.tariff.get().id), item.branding.backgroundColor)}">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtTariff"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="@{item.tariffName}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body2"
|
||||
app:layout_constraintBottom_toTopOf="@+id/txtProvider"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
@@ -57,12 +56,11 @@
|
||||
android:id="@+id/txtProvider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="@{item.provider}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:goneUnless="@{!item.tariffName.toLowerCase().startsWith(item.provider.toLowerCase())}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/rvTags"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtTariff"
|
||||
tools:text="Cheap Charging Co." />
|
||||
@@ -71,11 +69,10 @@
|
||||
android:id="@+id/rvTags"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:nestedScrollingEnabled="false"
|
||||
app:data="@{item.tags}"
|
||||
app:layout_constraintBottom_toTopOf="@+id/txtProviderCustomerTariff"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtProvider"
|
||||
tools:itemCount="1"
|
||||
@@ -85,12 +82,12 @@
|
||||
android:id="@+id/txtProviderCustomerTariff"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="@string/chargeprice_provider_customer_tariff"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:goneUnless="@{item.providerCustomerTariff}"
|
||||
app:layout_constraintBottom_toTopOf="@id/txtMonthlyFee"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/rvTags" />
|
||||
|
||||
@@ -98,12 +95,11 @@
|
||||
android:id="@+id/txtMonthlyFee"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:text="@{item.formatMonthlyFees(context)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:goneUnless="@{item.totalMonthlyFee > 0 || item.monthlyMinSales > 0}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintEnd_toStartOf="@+id/ivLogo"
|
||||
app:layout_constraintStart_toStartOf="@+id/txtTariff"
|
||||
app:layout_constraintTop_toBottomOf="@+id/txtProviderCustomerTariff"
|
||||
tools:text="Base fee 1 €/month" />
|
||||
@@ -160,5 +156,19 @@
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.65" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivLogo"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_margin="8dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:goneUnless="@{item.branding.logoUrl != null}"
|
||||
app:imageUrl="@{item.branding.logoUrl}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/guideline5"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:tintNullable="@{BindingAdaptersKt.isDarkMode(context) ? @android:color/white : null}"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="100dp"
|
||||
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek).toString().equals("closed") ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
|
||||
android:text="@{hours.getHoursForDayOfWeek(dayOfWeek) == null ? @string/closed_unfmt : hours.getHoursForDayOfWeek(dayOfWeek).toString()}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_help"
|
||||
android:icon="@drawable/ic_help"
|
||||
android:title="@string/help"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_close"
|
||||
android:icon="@drawable/ic_close"
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<string name="closed_opensat"><![CDATA[<b>Geschlossen</b> · Öffnet um %s]]></string>
|
||||
<string name="cost">Kosten</string>
|
||||
<string name="cost_detail"><![CDATA[<b>Laden:</b> %s · <b>Parken:</b> %s]]></string>
|
||||
<string name="cost_detail_charging"><![CDATA[<b>%s laden</b>]]></string>
|
||||
<string name="cost_detail_parking"><![CDATA[<b>%s parken</b>]]></string>
|
||||
<string name="free">Kostenlos</string>
|
||||
<string name="paid">Kostenpflichtig</string>
|
||||
<string name="amenities">Ladeweile</string>
|
||||
@@ -192,7 +194,7 @@
|
||||
<string name="chargeprice_vehicle">Fahrzeug</string>
|
||||
<string name="edit_on_goingelectric_info">Falls hier nur eine leere Seite erscheint, logge dich bitte zuerst bei GoingElectric.de ein.</string>
|
||||
<string name="close">schließen</string>
|
||||
<string name="chargeprice_title">Preisvergleich</string>
|
||||
<string name="chargeprice_title">Preise</string>
|
||||
<string name="chargeprice_connection_error">Preise konnten nicht geladen werden</string>
|
||||
<string name="chargeprice_no_compatible_connectors">Keiner der Anschlüsse dieser Ladestation ist mit deinem Fahrzeug kompatibel.</string>
|
||||
<string name="pref_chargeprice_currency">Währung</string>
|
||||
@@ -241,9 +243,11 @@
|
||||
<string name="unnamed_filter_profile">Unbenanntes Filterprofil</string>
|
||||
<string name="privacy_link">https://evmap.vonforst.net/de/privacy.html</string>
|
||||
<string name="faq_link">https://evmap.vonforst.net/de/faq.html</string>
|
||||
<string name="chargeprice_faq_link">https://evmap.vonforst.net/de/chargeprice_faq.html</string>
|
||||
<string name="required">erforderlich</string>
|
||||
<string name="edit_filter_profile">„%s“ bearbeiten</string>
|
||||
<string name="pref_search_delete_recent">Suchverlauf löschen</string>
|
||||
<string name="deleted_recent_search_results">Suchverlauf wurde gelöscht</string>
|
||||
<string name="settings_data_sources">Datenquellen</string>
|
||||
<string name="help">Hilfe</string>
|
||||
</resources>
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
<string name="holiday">Holiday</string>
|
||||
<string name="cost">Cost</string>
|
||||
<string name="cost_detail"><![CDATA[<b>Charging:</b> %s · <b>Parking:</b> %s]]></string>
|
||||
<string name="cost_detail_charging"><![CDATA[<b>%s charging</b>]]></string>
|
||||
<string name="cost_detail_parking"><![CDATA[<b>%s parking</b>]]></string>
|
||||
<string name="free">Free</string>
|
||||
<string name="paid">Paid</string>
|
||||
<string name="amenities">Amenities</string>
|
||||
@@ -226,9 +228,11 @@
|
||||
<string name="unnamed_filter_profile">Unnamed filter profile</string>
|
||||
<string name="privacy_link">https://evmap.vonforst.net/en/privacy.html</string>
|
||||
<string name="faq_link">https://evmap.vonforst.net/en/faq.html</string>
|
||||
<string name="chargeprice_faq_link">https://evmap.vonforst.net/en/chargeprice_faq.html</string>
|
||||
<string name="required">required</string>
|
||||
<string name="edit_filter_profile">Edit “%s”</string>
|
||||
<string name="pref_search_delete_recent">Delete recent search results</string>
|
||||
<string name="deleted_recent_search_results">Recent search results have been deleted</string>
|
||||
<string name="settings_data_sources">Data sources</string>
|
||||
<string name="help">Help</string>
|
||||
</resources>
|
||||
|
||||
@@ -8,13 +8,16 @@ class AvailabilityDetectorTest {
|
||||
@Test
|
||||
fun testMatchChargepointsSingleCorrect() {
|
||||
// single charger with 2 22kW chargepoints
|
||||
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
|
||||
val chargepoints = listOf(Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2))
|
||||
|
||||
// correct data in NewMotion
|
||||
assertEquals(
|
||||
mapOf(chargepoints[0] to setOf(0L, 1L)),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(0L to (22.0 to "Typ2"), 1L to (22.0 to "Typ2")),
|
||||
mapOf(
|
||||
0L to (22.0 to Chargepoint.TYPE_2_UNKNOWN),
|
||||
1L to (22.0 to Chargepoint.TYPE_2_UNKNOWN)
|
||||
),
|
||||
chargepoints
|
||||
)
|
||||
)
|
||||
@@ -23,13 +26,16 @@ class AvailabilityDetectorTest {
|
||||
@Test
|
||||
fun testMatchChargepointsSingleWrongPower() {
|
||||
// single charger with 2 22kW chargepoints
|
||||
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
|
||||
val chargepoints = listOf(Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2))
|
||||
|
||||
// wrong power in NewMotion
|
||||
assertEquals(
|
||||
mapOf(chargepoints[0] to setOf(0L, 1L)),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2")),
|
||||
mapOf(
|
||||
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
|
||||
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
|
||||
),
|
||||
chargepoints
|
||||
)
|
||||
)
|
||||
@@ -38,11 +44,15 @@ class AvailabilityDetectorTest {
|
||||
@Test(expected = AvailabilityDetectorException::class)
|
||||
fun testMatchChargepointsSingleWrong() {
|
||||
// single charger with 2 22kW chargepoints
|
||||
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
|
||||
val chargepoints = listOf(Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2))
|
||||
|
||||
// non-matching data in NewMotion
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2"), 2L to (50.0 to "CCS")),
|
||||
mapOf(
|
||||
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
|
||||
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
|
||||
2L to (50.0 to Chargepoint.CCS_UNKNOWN)
|
||||
),
|
||||
chargepoints
|
||||
)
|
||||
}
|
||||
@@ -51,11 +61,11 @@ class AvailabilityDetectorTest {
|
||||
fun testMatchChargepointsComplex() {
|
||||
// charger with many different connectors
|
||||
val chargepoints = listOf(
|
||||
Chargepoint("Typ2", 43.0, 1),
|
||||
Chargepoint("CCS", 50.0, 1),
|
||||
Chargepoint("CHAdeMO", 50.0, 2),
|
||||
Chargepoint("CCS", 160.0, 1),
|
||||
Chargepoint("CCS", 320.0, 2)
|
||||
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 43.0, 1),
|
||||
Chargepoint(Chargepoint.CCS_UNKNOWN, 50.0, 1),
|
||||
Chargepoint(Chargepoint.CHADEMO, 50.0, 2),
|
||||
Chargepoint(Chargepoint.CCS_UNKNOWN, 160.0, 1),
|
||||
Chargepoint(Chargepoint.CCS_UNKNOWN, 320.0, 2)
|
||||
)
|
||||
|
||||
// partly wrong power in NewMotion
|
||||
@@ -70,15 +80,15 @@ class AvailabilityDetectorTest {
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(
|
||||
// CHAdeMO + CCS HPC
|
||||
0L to (50.0 to "CHAdeMO"),
|
||||
1L to (200.0 to "CCS"),
|
||||
0L to (50.0 to Chargepoint.CHADEMO),
|
||||
1L to (200.0 to Chargepoint.CCS_UNKNOWN),
|
||||
// dual CCS HPC
|
||||
2L to (80.0 to "CCS"),
|
||||
3L to (200.0 to "CCS"),
|
||||
2L to (80.0 to Chargepoint.CCS_UNKNOWN),
|
||||
3L to (200.0 to Chargepoint.CCS_UNKNOWN),
|
||||
// 50kW triple charger
|
||||
4L to (50.0 to "CCS"),
|
||||
5L to (50.0 to "CHAdeMO"),
|
||||
6L to (43.0 to "Typ2")
|
||||
4L to (50.0 to Chargepoint.CCS_UNKNOWN),
|
||||
5L to (50.0 to Chargepoint.CHADEMO),
|
||||
6L to (43.0 to Chargepoint.TYPE_2_UNKNOWN)
|
||||
),
|
||||
chargepoints
|
||||
)
|
||||
@@ -89,15 +99,18 @@ class AvailabilityDetectorTest {
|
||||
fun testMatchChargepointsDifferentPower() {
|
||||
// single charger with 1 22kW and 1 11kW chargepoint (common when load balancing is applied)
|
||||
val chargepoints = listOf(
|
||||
Chargepoint("Typ2", 22.0, 1),
|
||||
Chargepoint("Typ2", 11.0, 1)
|
||||
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 1),
|
||||
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 11.0, 1)
|
||||
)
|
||||
|
||||
// both have 27 kW power in NewMotion
|
||||
assertEquals(
|
||||
mapOf(chargepoints[1] to setOf(0L), chargepoints[0] to setOf(1L)),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2")),
|
||||
mapOf(
|
||||
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
|
||||
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
|
||||
),
|
||||
chargepoints
|
||||
)
|
||||
)
|
||||
@@ -107,8 +120,8 @@ class AvailabilityDetectorTest {
|
||||
fun testMatchChargepointsDifferentPower2() {
|
||||
// two chargers with 1 22kW and 1 11kW chargepoint (common when load balancing is applied)
|
||||
val chargepoints = listOf(
|
||||
Chargepoint("Typ2", 22.0, 2),
|
||||
Chargepoint("Typ2", 11.0, 2)
|
||||
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2),
|
||||
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 11.0, 2)
|
||||
)
|
||||
|
||||
// both have 27 kW power in NewMotion
|
||||
@@ -116,10 +129,54 @@ class AvailabilityDetectorTest {
|
||||
mapOf(chargepoints[1] to setOf(0L, 1L), chargepoints[0] to setOf(2L, 3L)),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(
|
||||
0L to (27.0 to "Typ2"),
|
||||
1L to (27.0 to "Typ2"),
|
||||
2L to (27.0 to "Typ2"),
|
||||
3L to (27.0 to "Typ2")
|
||||
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
|
||||
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
|
||||
2L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
|
||||
3L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
|
||||
),
|
||||
chargepoints
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testMatchChargepointsMissingSchuko() {
|
||||
// single charger with 2 22kw chargepoints and two Schuko sockets
|
||||
val chargepoints = listOf(
|
||||
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 2),
|
||||
Chargepoint(Chargepoint.SCHUKO, 2.3, 2)
|
||||
)
|
||||
|
||||
// NewMotion only includes the Type 2 sockets
|
||||
assertEquals(
|
||||
mapOf(chargepoints[0] to setOf(0L, 1L)),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(
|
||||
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
|
||||
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
|
||||
),
|
||||
chargepoints
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMatchChargepointsMissingSchukoDifferentPower() {
|
||||
// single charger with 2 22kw chargepoints with load balancing and two Schuko sockets
|
||||
val chargepoints = listOf(
|
||||
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 22.0, 1),
|
||||
Chargepoint(Chargepoint.TYPE_2_UNKNOWN, 11.0, 1),
|
||||
Chargepoint(Chargepoint.SCHUKO, 2.3, 2)
|
||||
)
|
||||
|
||||
// NewMotion only includes the Type 2 sockets
|
||||
assertEquals(
|
||||
mapOf(chargepoints[1] to setOf(0L), chargepoints[0] to setOf(1L)),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(
|
||||
0L to (27.0 to Chargepoint.TYPE_2_UNKNOWN),
|
||||
1L to (27.0 to Chargepoint.TYPE_2_UNKNOWN)
|
||||
),
|
||||
chargepoints
|
||||
)
|
||||
|
||||
10
build.gradle
10
build.gradle
@@ -2,15 +2,15 @@
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.5.31'
|
||||
ext.about_libs_version = '8.8.5'
|
||||
ext.nav_version = '2.4.0-alpha10'
|
||||
ext.about_libs_version = '8.9.4'
|
||||
ext.nav_version = '2.4.0-beta02'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.2'
|
||||
classpath 'com.android.tools.build:gradle:7.0.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:$about_libs_version"
|
||||
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
|
||||
@@ -27,10 +27,6 @@ allprojects {
|
||||
//noinspection JcenterRepositoryObsolete
|
||||
jcenter() // still required for https://github.com/kamikat/moshi-jsonapi
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
|
||||
flatDir {
|
||||
dirs 'libs'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
6
fastlane/metadata/android/de-DE/changelogs/65.txt
Normal file
6
fastlane/metadata/android/de-DE/changelogs/65.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Verbesserungen:
|
||||
- Google Maps: neue Rendering-Engine für bessere Performance
|
||||
- Gelegentlich falsche Markerfarben bei Filter nach Anschlüssen behoben
|
||||
- Android Auto: Distanz wird automatisch in passenden Einheiten angezeigt (m, km, mi, ft, yd)
|
||||
- Android Auto: Aktualisierungsfrequenz reduziert um störende Animation zu vermeiden
|
||||
- Verschiedene Abstürze behoben
|
||||
11
fastlane/metadata/android/de-DE/changelogs/66.txt
Normal file
11
fastlane/metadata/android/de-DE/changelogs/66.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Verbesserungen:
|
||||
- Echtzeitdaten für weitere AC-Ladestationen
|
||||
- FAQ-Seite zum Preisvergleich
|
||||
- GoingElectric: irreführendes "Laden/Parken: kostenpflichtig" entfernt
|
||||
|
||||
Fehlerbehebungen:
|
||||
- GoingElectric: Beschreibung zu Öffnungszeiten wurde nicht immer angezeigt
|
||||
- Leere Detailansicht, nachdem die App längere Zeit im Hintergrund war
|
||||
- Preisvergleich: Exklusive Energiekunden-Tarife unter "Meine Tarife" wurden nicht angezeigt
|
||||
- Eingestellte Sprache wurde nicht für Google Maps genutzt
|
||||
- Abstürze behoben
|
||||
6
fastlane/metadata/android/en-US/changelogs/65.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/65.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
Improvements:
|
||||
- Google Maps: new rendering engine for better performance
|
||||
- Fixed occasionally wrong marker colors when filtering by connectors
|
||||
- Android Auto: Distance will automatically use suitable units (m, km, mi, ft, yd)
|
||||
- Android Auto: Reduced refresh frequency to avoid annoying animation
|
||||
- Fixed various crashes
|
||||
11
fastlane/metadata/android/en-US/changelogs/66.txt
Normal file
11
fastlane/metadata/android/en-US/changelogs/66.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
Improvements:
|
||||
- Realtime data for additional AC chargers
|
||||
- FAQ page about price comparison
|
||||
- GoingElectric: removed misleading "charging/parking: paid"
|
||||
|
||||
Bug fixes:
|
||||
- GoingElectric: Description text for opening hours was not always shown
|
||||
- Empty detail view after app was in background for a longer time
|
||||
- Price comparison: provider exclusive plans were not shown even if selected under "my plans"
|
||||
- Selected language was not used for Google Maps
|
||||
- Fixed various crashes
|
||||
Reference in New Issue
Block a user