Compare commits

...

19 Commits
1.6.0 ... 1.6.4

Author SHA1 Message Date
johan12345
06801c1898 Release 1.6.4 2023-06-17 17:59:30 +02:00
johan12345
c946b0fcd3 TeslaAvailabilityDetector: fix cases when number of chargepoints does not match 2023-06-17 17:38:33 +02:00
johan12345
dd4fcc7550 Run clustering on Dispatchers.Default, not Dispatchers.IO 2023-06-16 23:08:01 +02:00
johan12345
2ce82b961b Introduce clustering up to zoom level 15 in very crowded places (>500 chargers within view)
refs #285
2023-06-16 22:50:14 +02:00
johan12345
1be519b1ee Release 1.6.3 2023-06-14 22:02:07 +02:00
Hosted Weblate
01737f21d2 Translated using Weblate (Portuguese)
Currently translated at 100.0% (313 of 313 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2023-06-14 21:07:41 +02:00
johan12345
17ce9f420b Tesla: CongestionPriceHistogram is nullable 2023-06-13 22:57:10 +02:00
johan12345
6eb90498eb GoingElectric: fix SQL implementation of network/barrierFree/chargeCards filters 2023-06-13 20:37:30 +02:00
johan12345
074e0bf904 Release 1.6.2 2023-06-12 08:35:02 +02:00
johan12345
41ac223e97 properly escape strings in SQL queries 2023-06-12 08:33:33 +02:00
Hosted Weblate
f7196bcce0 Translated using Weblate (Portuguese)
Currently translated at 100.0% (313 of 313 strings)

Co-authored-by: Celso Azevedo <mail@celsoazevedo.com>
Translate-URL: https://hosted.weblate.org/projects/evmap/android/pt/
Translation: EVMap/Android
2023-06-11 21:56:23 +02:00
johan12345
4f6092e5dc remove extra spatialite library 2023-06-11 21:20:38 +02:00
johan12345
dfd42e1ffd upgrade spatia-room 2023-06-11 21:17:31 +02:00
johan12345
895b24d406 Release 1.6.1 2023-06-11 20:16:26 +02:00
johan12345
3dea7993f3 clear cache with next update 2023-06-11 19:54:36 +02:00
johan12345
ca90f1b37f GoingElectricApi: infer some details based on applied filters 2023-06-11 19:34:19 +02:00
johan12345
fe0843e653 fix bug in caching algorithm that caused chargers to disappear
some filters require details that we do not get in normal queries
2023-06-11 19:19:37 +02:00
johan12345
0f42ae84de fix NPE 2023-06-11 19:17:53 +02:00
johan12345
2748b0a3db make faultReport: true result in non-null value 2023-06-11 19:05:33 +02:00
21 changed files with 209 additions and 74 deletions

View File

@@ -21,8 +21,8 @@ android {
minSdkVersion 21
targetSdkVersion 33
// NOTE: always increase versionCode by 2 since automotive flavor uses versionCode + 1
versionCode 180
versionName "1.6.0"
versionCode 188
versionName "1.6.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
resConfigs supportedLocales.split(',')
@@ -246,10 +246,7 @@ dependencies {
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
implementation('com.github.anboralabs:spatia-room:0.2.6') {
exclude group: 'com.github.dalgarins', module: 'android-spatialite'
}
implementation 'com.github.ev-map:android-spatialite:654dca2365'
implementation 'com.github.anboralabs:spatia-room:0.2.7'
// billing library
def billing_version = "6.0.0"

View File

@@ -49,6 +49,8 @@ interface ChargepointApi<out T : ReferenceData> {
fun convertFiltersToSQL(filters: FilterValues, referenceData: ReferenceData): FiltersSQLQuery
fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean
val name: String
val id: String

View File

@@ -281,7 +281,7 @@ interface TeslaGraphQlApi {
val siteDynamic: SiteDynamic,
val siteStatic: SiteStatic,
val pricing: Pricing,
val congestionPriceHistogram: CongestionPriceHistogram,
val congestionPriceHistogram: CongestionPriceHistogram?,
)
@JsonClass(generateAdapter = true)
@@ -540,6 +540,9 @@ class TeslaAvailabilityDetector(
Chargepoint.CCS_UNKNOWN
) && it.power != null && it.power <= 150
}
if (scV2CCSConnectors.sumOf { it.count } != 0 && scV2CCSConnectors.sumOf { it.count } != scV2Connectors.sumOf { it.count }) {
throw AvailabilityDetectorException("number of V2 connectors does not match number of V2 CCS connectors")
}
val scV3Connectors = location.chargepoints.filter {
it.type in listOf(
Chargepoint.CCS_TYPE_2,
@@ -550,36 +553,49 @@ class TeslaAvailabilityDetector(
"charger has unknown connectors"
)
val statusSorted = details.siteDynamic.chargerDetails.sortedBy { it.charger.labelLetter }
.sortedBy { it.charger.labelNumber }
var statusSorted = details.siteDynamic.chargerDetails.sortedBy { it.charger.labelLetter }
.sortedBy { it.charger.labelNumber }.map { it.availability }
if (statusSorted.size != scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count }) {
// apparently some connectors are missing in Tesla data
// If we have just one type of charger, we can still match
val numMissing =
scV2Connectors.sumOf { it.count } + scV3Connectors.sumOf { it.count } - statusSorted.size
if (scV2Connectors.isEmpty() || scV3Connectors.isEmpty() && numMissing > 0) {
statusSorted =
statusSorted + List(numMissing) { TeslaGraphQlApi.ChargerAvailability.UNKNOWN }
} else {
throw AvailabilityDetectorException("Tesla API chargepoints do not match data source")
}
}
val statusMap = emptyMap<Chargepoint, List<ChargepointStatus>>().toMutableMap()
var i = 0
for (connector in scV2Connectors) {
statusMap[connector] =
statusSorted.subList(i, i + connector.count).map { it.availability.toStatus() }
statusSorted.subList(i, i + connector.count).map { it.toStatus() }
i += connector.count
}
if (scV2CCSConnectors.isNotEmpty()) {
i = 0
for (connector in scV2CCSConnectors) {
statusMap[connector] =
statusSorted.subList(i, i + connector.count).map { it.availability.toStatus() }
statusSorted.subList(i, i + connector.count).map { it.toStatus() }
i += connector.count
}
}
for (connector in scV3Connectors) {
statusMap[connector] =
statusSorted.subList(i, i + connector.count).map { it.availability.toStatus() }
statusSorted.subList(i, i + connector.count).map { it.toStatus() }
i += connector.count
}
val indexOfMidnight =
details.congestionPriceHistogram.dataAttributes.indexOfFirst { it.label == "12AM" }
val congestionHistogram = indexOfMidnight.takeIf { it >= 0 }?.let { index ->
val data = details.congestionPriceHistogram.data.toMutableList()
Collections.rotate(data, -index)
data
val congestionHistogram = details.congestionPriceHistogram?.let { cph ->
val indexOfMidnight = cph.dataAttributes.indexOfFirst { it.label == "12AM" }
indexOfMidnight.takeIf { it >= 0 }?.let { index ->
val data = cph.data.toMutableList()
Collections.rotate(data, -index)
data
}
}
return ChargeLocationStatus(

View File

@@ -96,7 +96,7 @@ internal class JsonObjectOrFalseAdapter<T> private constructor(
false -> null // Response was false
else -> {
if (this.clazz == GEFaultReport::class.java) {
GEFaultReport(null, null) as T
GEFaultReport(null, "") as T
} else {
throw IllegalStateException("Non-false boolean for @JsonObjectOrFalse field")
}

View File

@@ -1,6 +1,7 @@
package net.vonforst.evmap.api.goingelectric
import android.content.Context
import android.database.DatabaseUtils
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.squareup.moshi.Moshi
@@ -217,7 +218,7 @@ class GoingElectricApiWrapper(
}
} while (startkey != null && startkey < 10000)
val result = postprocessResult(data, minPower, connectorsVal, minConnectors)
val result = postprocessResult(data, filters)
return Resource.success(ChargepointList(result, startkey == null))
}
@@ -308,18 +309,26 @@ class GoingElectricApiWrapper(
}
} while (startkey != null && startkey < 10000)
val result = postprocessResult(data, minPower, connectorsVal, minConnectors)
val result = postprocessResult(data, filters)
return Resource.success(ChargepointList(result, startkey == null))
}
private fun postprocessResult(
chargers: List<GEChargepointListItem>,
minPower: Int?,
connectorsVal: MultipleChoiceFilterValue?,
minConnectors: Int?
filters: FilterValues?
): List<ChargepointListItem> {
// apply filters which GoingElectric does not support natively
val minPower = filters?.getSliderValue("min_power")
val minConnectors = filters?.getSliderValue("min_connectors")
val connectorsVal = filters?.getMultipleChoiceValue("connectors")
val freecharging = filters?.getBooleanValue("freecharging")
val freeparking = filters?.getBooleanValue("freeparking")
val open247 = filters?.getBooleanValue("open_247")
val barrierfree = filters?.getBooleanValue("barrierfree")
val networks = filters?.getMultipleChoiceValue("networks")
val chargecards = filters?.getMultipleChoiceValue("chargecards")
return chargers.filter { it ->
// apply filters which GoingElectric does not support natively
if (it is GEChargeLocation) {
it.chargepoints
.filter { it.power >= (minPower ?: 0) }
@@ -328,6 +337,40 @@ class GoingElectricApiWrapper(
} else {
true
}
}.map {
// infer some properties based on applied filters
if (it is GEChargeLocation) {
var inferred = it
if (freecharging == true) {
inferred = inferred.copy(
cost = inferred.cost?.copy(freecharging = true)
?: GECost(freecharging = true)
)
}
if (freeparking == true) {
inferred = inferred.copy(
cost = inferred.cost?.copy(freeparking = true) ?: GECost(freeparking = true)
)
}
if (open247 == true) {
inferred = inferred.copy(
openinghours = inferred.openinghours?.copy(twentyfourSeven = true)
?: GEOpeningHours(twentyfourSeven = true)
)
}
if (barrierfree == true
&& (networks == null || networks.all || it.network !in networks.values)
&& (chargecards == null || chargecards.all)
) {
/* barrierfree, networks and chargecards are combined with OR - so we can only
* be sure that the charger is barrierFree if the other filters are not active
* or the charger does not match the other filters */
inferred = inferred.copy(barrierFree = true)
}
inferred
} else {
it
}
}.map { it.convert(apikey, false) }
}
@@ -487,9 +530,6 @@ class GoingElectricApiWrapper(
if (filters.getBooleanValue("open_247") == true) {
result.append(" AND twentyfourSeven IS 1")
}
if (filters.getBooleanValue("barrierfree") == true) {
result.append(" AND barrierFree IS 1")
}
if (filters.getBooleanValue("exclude_faults") == true) {
result.append(" AND fault_report_description IS NULL AND fault_report_created IS NULL")
}
@@ -505,31 +545,46 @@ class GoingElectricApiWrapper(
val connectorsList = if (connectors.values.size == 0) {
""
} else {
"'" + connectors.values.joinToString("', '") { GEChargepoint.convertTypeFromGE(it) } + "'"
connectors.values.joinToString(",") {
DatabaseUtils.sqlEscapeString(
GEChargepoint.convertTypeFromGE(
it
)
)
}
}
result.append(" AND json_extract(cp.value, '$.type') IN (${connectorsList})")
requiresChargepointQuery = true
}
// networks, chargecards and barrierFree filters are combined with OR in the GE API
val networks = filters.getMultipleChoiceValue("networks")
if (networks != null && !networks.all) {
val networksList = if (networks.values.size == 0) {
""
} else {
"'" + networks.values.joinToString("', '") + "'"
}
result.append(" AND network IN (${networksList})")
}
val chargecards = filters.getMultipleChoiceValue("chargecards")
if (chargecards != null && !chargecards.all) {
val chargecardsList = if (chargecards.values.size == 0) {
""
} else {
chargecards.values.joinToString(",")
val barrierFree = filters.getBooleanValue("barrierfree")
if ((networks != null && !networks.all) || barrierFree == true || (chargecards != null && !chargecards.all)) {
val queries = mutableListOf<String>()
if (networks != null && !networks.all) {
val networksList = if (networks.values.size == 0) {
""
} else {
networks.values.joinToString(",") { DatabaseUtils.sqlEscapeString(it) }
}
queries.add("network IN (${networksList})")
}
result.append(" AND json_extract(cc.value, '$.id') IN (${chargecardsList})")
requiresChargeCardQuery = true
if (barrierFree == true) {
queries.add("barrierFree IS 1")
}
if (chargecards != null && !chargecards.all) {
val chargecardsList = if (chargecards.values.size == 0) {
""
} else {
chargecards.values.joinToString(",")
}
queries.add("json_extract(cc.value, '$.id') IN (${chargecardsList})")
requiresChargeCardQuery = true
}
result.append(" AND (${queries.joinToString(" OR ")})")
}
val categories = filters.getMultipleChoiceValue("categories")
@@ -546,5 +601,14 @@ class GoingElectricApiWrapper(
return FiltersSQLQuery(result.toString(), requiresChargepointQuery, requiresChargeCardQuery)
}
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
val chargecards = filters.getMultipleChoiceValue("chargecards")
return filters.getBooleanValue("freecharging") == true
|| filters.getBooleanValue("freeparking") == true
|| filters.getBooleanValue("open_247") == true
|| filters.getBooleanValue("barrierfree") == true
|| (chargecards != null && !chargecards.all)
}
}

View File

@@ -86,10 +86,10 @@ data class GEChargeLocation(
@JsonClass(generateAdapter = true)
data class GECost(
val freecharging: Boolean,
val freeparking: Boolean,
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String?,
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String?
val freecharging: Boolean = false,
val freeparking: Boolean = false,
@JsonObjectOrFalse @Json(name = "description_short") val descriptionShort: String? = null,
@JsonObjectOrFalse @Json(name = "description_long") val descriptionLong: String? = null
) {
fun convert() = Cost(
// In GE, freecharging = false can either mean "paid charging" or "no information
@@ -104,8 +104,8 @@ data class GECost(
@JsonClass(generateAdapter = true)
data class GEOpeningHours(
@Json(name = "24/7") val twentyfourSeven: Boolean,
@JsonObjectOrFalse val description: String?,
val days: GEOpeningHoursDays?
@JsonObjectOrFalse val description: String? = null,
val days: GEOpeningHoursDays? = null
) {
fun convert() = OpeningHours(twentyfourSeven, description, days?.convert())
}

View File

@@ -1,6 +1,7 @@
package net.vonforst.evmap.api.openchargemap
import android.content.Context
import android.database.DatabaseUtils
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
import com.squareup.moshi.Moshi
@@ -347,12 +348,14 @@ class OpenChargeMapApiWrapper(
val connectorsList = if (connectors.values.size == 0) {
""
} else {
"'" + connectors.values.joinToString("', '") {
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
refData
connectors.values.joinToString(",") {
DatabaseUtils.sqlEscapeString(
OCMConnection.convertConnectionTypeFromOCM(
it.toLong(),
refData
)
)
} + "'"
}
}
result.append(" AND json_extract(cp.value, '$.type') IN (${connectorsList})")
requiresChargepointQuery = true
@@ -363,9 +366,9 @@ class OpenChargeMapApiWrapper(
val networksList = if (operators.values.size == 0) {
""
} else {
"'" + operators.values.joinToString("', '") { opId ->
refData.operators.find { it.id == opId.toLong() }?.title.orEmpty()
} + "'"
operators.values.joinToString(",") { opId ->
DatabaseUtils.sqlEscapeString(refData.operators.find { it.id == opId.toLong() }?.title.orEmpty())
}
}
result.append(" AND network IN (${networksList})")
}
@@ -379,4 +382,10 @@ class OpenChargeMapApiWrapper(
return FiltersSQLQuery(result.toString(), requiresChargepointQuery, false)
}
override fun filteringInSQLRequiresDetails(filters: FilterValues): Boolean {
val operators = filters.getMultipleChoiceValue("operators")
return (operators != null && !operators.all)
// TODO: it would be possible to implement this without requiring details if we extended the data structure to also save the operator ID in the DB
}
}

View File

@@ -101,7 +101,7 @@ data class OCMChargepoint(
connections.first { it.statusType != null && it.statusTypeId in faultStatuses }.statusType!!.title
)
}
return FaultReport(null, null)
return FaultReport(null, "")
} else {
return null
}

View File

@@ -867,7 +867,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
if (BuildConfig.FLAVOR.contains("google") && mapFragment!!.priority[0] == MapFragment.GOOGLE) {
// Google Maps: icons can be generated in background thread
lifecycleScope.launch {
withContext(Dispatchers.IO) {
withContext(Dispatchers.Default) {
chargerIconGenerator.preloadCache()
}
}

View File

@@ -161,6 +161,7 @@ class ChargeLocationsRepository(
val filtersSerialized =
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
?.serialize()
val requiresDetail = filters?.let { api.filteringInSQLRequiresDetails(it) } ?: false
val savedRegionResult = savedRegionDao.savedRegionCovers(
bounds.southwest.latitude,
bounds.northeast.latitude,
@@ -168,7 +169,8 @@ class ChargeLocationsRepository(
bounds.northeast.longitude,
api.id,
cacheSoftLimitDate(api),
filtersSerialized
filtersSerialized,
requiresDetail
)
val useClustering = shouldUseServerSideClustering(zoom)
val apiResult = liveData {
@@ -224,13 +226,15 @@ class ChargeLocationsRepository(
val filtersSerialized =
filters?.filter { it.value != it.filter.defaultValue() }?.takeIf { it.isNotEmpty() }
?.serialize()
val requiresDetail = filters?.let { api.filteringInSQLRequiresDetails(it) } ?: false
val savedRegionResult = savedRegionDao.savedRegionCoversRadius(
location.latitude,
location.longitude,
radiusMeters * 0.999, // to account for float rounding errors
api.id,
cacheSoftLimitDate(api),
filtersSerialized
filtersSerialized,
requiresDetail
)
val useClustering = shouldUseServerSideClustering(zoom)
val apiResult = liveData {
@@ -286,10 +290,14 @@ class ChargeLocationsRepository(
chargers: List<ChargeLocation>,
zoom: Float
): List<ChargepointListItem> {
/* in very crowded places (good example: central London on OpenChargeMap without filters)
we have to cluster even at pretty high zoom levels to make sure the map does not get
laggy. Otherwise, only cluster at zoom levels <= 11. */
val useClustering = chargers.size > 500 || zoom <= 11f
val clusterDistance = getClusterDistance(zoom)
val chargersClustered = if (clusterDistance != null) {
Dispatchers.IO.run {
val chargersClustered = if (useClustering && clusterDistance != null) {
Dispatchers.Default.run {
cluster(chargers, zoom, clusterDistance)
}
} else chargers

View File

@@ -4,22 +4,20 @@ import android.annotation.SuppressLint
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import androidx.lifecycle.map
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import co.anbora.labs.spatia.builder.SpatiaBuilder
import co.anbora.labs.spatia.builder.SpatiaRoom
import co.anbora.labs.spatia.geometry.GeometryConverters
import kotlinx.coroutines.runBlocking
import net.vonforst.evmap.api.goingelectric.GEChargeCard
import net.vonforst.evmap.api.goingelectric.GEChargepoint
import net.vonforst.evmap.api.openchargemap.OCMConnectionType
import net.vonforst.evmap.api.openchargemap.OCMCountry
import net.vonforst.evmap.api.openchargemap.OCMOperator
import net.vonforst.evmap.model.*
import net.vonforst.evmap.viewmodel.await
@Database(
entities = [
@@ -37,7 +35,7 @@ import net.vonforst.evmap.viewmodel.await
OCMCountry::class,
OCMOperator::class,
SavedRegion::class
], version = 20
], version = 21
)
@TypeConverters(Converters::class, GeometryConverters::class)
abstract class AppDatabase : RoomDatabase() {
@@ -77,7 +75,7 @@ abstract class AppDatabase : RoomDatabase() {
MIGRATION_2, MIGRATION_3, MIGRATION_4, MIGRATION_5, MIGRATION_6,
MIGRATION_7, MIGRATION_8, MIGRATION_9, MIGRATION_10, MIGRATION_11,
MIGRATION_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16,
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20
MIGRATION_17, MIGRATION_18, MIGRATION_19, MIGRATION_20, MIGRATION_21
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -447,6 +445,13 @@ abstract class AppDatabase : RoomDatabase() {
}
}
}
private val MIGRATION_21 = object : Migration(20, 21) {
override fun migrate(db: SupportSQLiteDatabase) {
// clear cache with this update
db.execSQL("DELETE FROM savedregion")
}
}
}
/**

View File

@@ -53,6 +53,7 @@ internal fun getClusterDistance(zoom: Float): Int? {
return when (zoom) {
in 0.0..7.0 -> 100
in 7.0..11.0 -> 75
in 11.0..15.0 -> 75
else -> null
}
}
@@ -548,7 +549,8 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
val apiId = apiId.value
when (apiId) {
"goingelectric" -> {
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")!!
val chargeCardsVal =
filters.getMultipleChoiceValue("chargecards") ?: return@addSource
filteredChargeCards.value =
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }
.toSet()

View File

@@ -58,7 +58,7 @@
<string name="fault_report_date">Com problemas (atualizado: %s)</string>
<string name="filter_chargecards">Formas de pagamento</string>
<string name="pref_language">Língua da app</string>
<string name="all_selected">Todos selecionados</string>
<string name="all_selected">Todas selecionadas</string>
<string name="edit">editar</string>
<string name="pref_darkmode">Modo escuro</string>
<string name="connection_error">Não foi possível carregar a lista de carregadores</string>
@@ -97,7 +97,7 @@
<string name="save_as_profile">Guardar como perfil</string>
<string name="filterprofiles_empty_state">Não existem filtros guardados</string>
<string name="welcome_2">Cada cor corresponde a potência máxima do carregador</string>
<string name="welcome_to_evmap">Bem-vindo ao EVMap</string>
<string name="welcome_to_evmap">Bem-vindo(a) ao EVMap</string>
<string name="pref_darkmode_always_off">Sempre desligado</string>
<string name="welcome_2_title">Escolha a potência</string>
<string name="navigate">Navegar</string>
@@ -302,7 +302,7 @@
<string name="charger_website">Website</string>
<string name="realtime_data_login_needed">Conta Tesla necessária para informação em tempo real</string>
<string name="charge_price_minute_format">%2$s%1$.2f/min</string>
<string name="pref_tesla_account_disabled">Faça o login para ver os dados em tempo real dos Superchargers da Tesla. Não é necessário possuir um veículo da Tesla</string>
<string name="pref_tesla_account_disabled">Faça o login para ver informação em tempo real sobre os Tesla Superchargers. Não é necessário possuir um veículo Tesla</string>
<string name="login">Login</string>
<string name="login_error">Falha no login</string>
<string name="pricing_up_to">até %s</string>
@@ -324,4 +324,10 @@
<string name="pref_map_scale_meters">metros</string>
<string name="pref_map_scale_miles">milhas</string>
<string name="pref_map_scale">Barra de escala do mapa</string>
<string name="data_retrieved_at">Informação atualizada %s</string>
<string name="settings_cache_count">Tamanho da cache</string>
<string name="settings_cache_clear">Limpar cache</string>
<string name="settings_cache_count_summary">%d carregadores na base de dados, %.1f MB</string>
<string name="settings_caching">Caching (base de dados local)</string>
<string name="settings_cache_clear_summary">Elimina todos os carregadores guardados na base de dados local, com a exceção dos seus favoritos</string>
</resources>

View File

@@ -0,0 +1,3 @@
Fehler behoben:
- Abstürze behoben
- Fehler im Caching-Algorithmus im Zusammenspiel mit bestimmten Filtern behoben

View File

@@ -0,0 +1,2 @@
Fehler behoben:
- Abstürze im Zusammenspiel mit bestimmten Filtern behoben

View File

@@ -0,0 +1,3 @@
Fehler behoben:
- Fehler im Caching-Algorithmus im Zusammenspiel mit bestimmten Filtern behoben
- Abstürze behoben

View File

@@ -0,0 +1,5 @@
Verbesserungen:
- Clustering an Orten mit extrem hoher Ladestationsdichte verstärkt
Fehler behoben:
- Abstürze behoben

View File

@@ -0,0 +1,3 @@
Bugfixes:
- Fixed crashes
- Fixed error in caching algorithm when some filters are active

View File

@@ -0,0 +1,2 @@
Bugfixes:
- Fixed crashes when some filters are active

View File

@@ -0,0 +1,3 @@
Bugfixes:
- Fixed error in caching algorithm when some filters are active
- Fixed crashes

View File

@@ -0,0 +1,5 @@
Improvements:
- Increased clustering in places with extremely high charger density
Bugfixes:
- Fixed crashes