Merge pull request #157 from johan12345/db-restructure

Database restructuring in preparation for new features
This commit is contained in:
Johan von Forstner
2022-01-30 14:17:11 +01:00
committed by GitHub
14 changed files with 177 additions and 55 deletions

View File

@@ -268,12 +268,13 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
try {
// load chargers
if (favorites) {
chargers = db.chargeLocationsDao().getAllChargeLocationsAsync().sortedBy {
distanceBetween(
location.latitude, location.longitude,
it.coordinates.lat, it.coordinates.lng
)
}
chargers =
db.favoritesDao().getAllFavoritesAsync().map { it.charger }.sortedBy {
distanceBetween(
location.latitude, location.longitude,
it.coordinates.lat, it.coordinates.lng
)
}
} else {
val response = api.getChargepointsRadius(
referenceData,

View File

@@ -14,7 +14,7 @@ class FavoritesAdapter(val onDelete: (FavoritesViewModel.FavoritesListItem) -> U
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
override fun getItemId(position: Int): Long = getItem(position).charger.id
override fun getItemId(position: Int): Long = getItem(position).fav.favorite.favoriteId
@SuppressLint("ClickableViewAccessibility")
override fun bind(

View File

@@ -326,7 +326,7 @@ class GoingElectricApiWrapper(
} else {
true
}
}.map { it.convert(apikey) }
}.map { it.convert(apikey, false) }
// apply clustering
val useClustering = zoom < 13
@@ -350,7 +350,7 @@ class GoingElectricApiWrapper(
return if (response.isSuccessful && response.body()!!.status == "ok" && response.body()!!.chargelocations.size == 1) {
Resource.success(
(response.body()!!.chargelocations[0] as GEChargeLocation).convert(
apikey
apikey, true
)
)
} else {

View File

@@ -29,7 +29,7 @@ data class GEChargeCardList(
)
sealed class GEChargepointListItem {
abstract fun convert(apikey: String): ChargepointListItem
abstract fun convert(apikey: String, isDetailed: Boolean): ChargepointListItem
}
@JsonClass(generateAdapter = true)
@@ -54,7 +54,7 @@ data class GEChargeLocation(
val openinghours: GEOpeningHours?,
val cost: GECost?
) : GEChargepointListItem() {
override fun convert(apikey: String) = ChargeLocation(
override fun convert(apikey: String, isDetailed: Boolean) = ChargeLocation(
id,
"goingelectric",
name,
@@ -76,7 +76,9 @@ data class GEChargeLocation(
openinghours?.convert(),
cost?.convert(),
null,
ChargepriceData(address.country, network, chargepoints.map { it.type })
ChargepriceData(address.country, network, chargepoints.map { it.type }),
Instant.now(),
isDetailed
)
}
@@ -161,7 +163,7 @@ data class GEChargeLocationCluster(
val clusterCount: Int,
val coordinates: GECoordinate
) : GEChargepointListItem() {
override fun convert(apikey: String) =
override fun convert(apikey: String, isDetailed: Boolean) =
ChargeLocationCluster(clusterCount, coordinates.convert())
}

View File

@@ -235,7 +235,7 @@ class OpenChargeMapApiWrapper(
.filter { it.power == null || it.power >= (minPower ?: 0.0) }
.filter { if (connectorsVal != null && !connectorsVal.all) it.connectionTypeId in connectorsVal.values.map { it.toLong() } else true }
.sumOf { it.quantity ?: 1 } >= (minConnectors ?: 0)
}.map { it.convert(referenceData) }.distinct() as List<ChargepointListItem>
}.map { it.convert(referenceData, false) }.distinct() as List<ChargepointListItem>
// apply clustering
val useClustering = zoom < 13
@@ -256,7 +256,7 @@ class OpenChargeMapApiWrapper(
try {
val response = api.getChargepointDetail(id)
if (response.isSuccessful && response.body()?.size == 1) {
return Resource.success(response.body()!![0].convert(referenceData))
return Resource.success(response.body()!![0].convert(referenceData, true))
} else {
return Resource.error(response.message(), null)
}

View File

@@ -7,6 +7,7 @@ import com.squareup.moshi.JsonClass
import kotlinx.parcelize.Parcelize
import net.vonforst.evmap.max
import net.vonforst.evmap.model.*
import java.time.Instant
import java.time.ZonedDateTime
// Unknown, Currently Available, Currently In Use, Operational
@@ -44,7 +45,7 @@ data class OCMChargepoint(
@Json(name = "UserComments") val userComments: List<OCMUserComment>?,
@Json(name = "DateLastStatusUpdate") val lastStatusUpdateDate: ZonedDateTime?
) {
fun convert(refData: OCMReferenceData) = ChargeLocation(
fun convert(refData: OCMReferenceData, isDetailed: Boolean) = ChargeLocation(
id,
"openchargemap",
addressInfo.title,
@@ -69,7 +70,9 @@ data class OCMChargepoint(
ChargepriceData(
addressInfo.countryISOCode(refData),
operatorId?.toString(),
connections.map { "${it.connectionTypeId},${it.currentTypeId}" })
connections.map { "${it.connectionTypeId},${it.currentTypeId}" }),
Instant.now(),
isDetailed
)
private fun convertFaultReport(): FaultReport? {

View File

@@ -28,7 +28,8 @@ import net.vonforst.evmap.adapter.DataBindingAdapter
import net.vonforst.evmap.adapter.FavoritesAdapter
import net.vonforst.evmap.databinding.FragmentFavoritesBinding
import net.vonforst.evmap.databinding.ItemFavoriteBinding
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.model.FavoriteWithDetail
import net.vonforst.evmap.utils.checkAnyLocationPermission
import net.vonforst.evmap.viewmodel.FavoritesViewModel
import net.vonforst.evmap.viewmodel.viewModelFactory
@@ -36,7 +37,7 @@ import net.vonforst.evmap.viewmodel.viewModelFactory
class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
private lateinit var binding: FragmentFavoritesBinding
private var locationClient: LostApiClient? = null
private var toDelete: ChargeLocation? = null
private var toDelete: Favorite? = null
private var deleteSnackbar: Snackbar? = null
private lateinit var adapter: FavoritesAdapter
@@ -84,7 +85,7 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
)
adapter = FavoritesAdapter(onDelete = {
delete(it.charger)
delete(it.fav)
}).apply {
onClickListener = {
findNavController().navigate(
@@ -132,18 +133,20 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
}
}
fun delete(fav: ChargeLocation) {
val position = vm.listData.value?.indexOfFirst { it.charger == fav } ?: return
fun delete(fav: FavoriteWithDetail) {
val position =
vm.listData.value?.indexOfFirst { it.fav.favorite.favoriteId == fav.favorite.favoriteId }
?: return
// if there is already a profile to delete, delete it now
actuallyDelete()
deleteSnackbar?.dismiss()
toDelete = fav
toDelete = fav.favorite
view?.let {
val snackbar = Snackbar.make(
it,
getString(R.string.deleted_filterprofile, fav.name),
getString(R.string.deleted_filterprofile, fav.charger.name),
Snackbar.LENGTH_LONG
).setAction(R.string.undo) {
toDelete = null
@@ -182,7 +185,7 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val fav = vm.favorites.value?.find { it.id == viewHolder.itemId }
val fav = vm.favorites.value?.find { it.favorite.favoriteId == viewHolder.itemId }
fav?.let { delete(it) }
}

View File

@@ -499,9 +499,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun toggleFavorite() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
val isFav = favs.find { it.id == charger.id } != null
if (isFav) {
vm.deleteFavorite(charger)
val fav = favs.find { it.charger.id == charger.id }
if (fav != null) {
vm.deleteFavorite(fav.favorite)
} else {
vm.insertFavorite(charger)
}
@@ -511,7 +511,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = !isFav
fav = fav == null
)
)
}
@@ -642,7 +642,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.id } ?: emptyList()
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
)
)
}
@@ -657,7 +657,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
)
)
animator.animateMarkerBounce(marker)
@@ -671,7 +671,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.id } ?: emptyList()
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
)
)
}
@@ -681,7 +681,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private fun updateFavoriteToggle() {
val favs = vm.favorites.value ?: return
val charger = vm.chargerSparse.value ?: return
if (favs.find { it.id == charger.id } != null) {
if (favs.find { it.charger.id == charger.id } != null) {
favToggle.setIcon(R.drawable.ic_fav)
} else {
favToggle.setIcon(R.drawable.ic_fav_no)
@@ -1023,7 +1023,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = charger == vm.chargerSparse.value,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
)
)
}
@@ -1041,7 +1041,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = charger.isMulti(vm.filteredConnectors.value)
val fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
val fav =
charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi, fav)
} else {
animator.deleteMarker(marker)
@@ -1057,7 +1058,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val highlight = charger == vm.chargerSparse.value
val fault = charger.faultReport != null
val multi = charger.isMulti(vm.filteredConnectors.value)
val fav = charger.id in vm.favorites.value?.map { it.id } ?: emptyList()
val fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
val marker = map.addMarker(
MarkerOptions()
.position(LatLng(charger.coordinates.lat, charger.coordinates.lng))

View File

@@ -24,6 +24,14 @@ import kotlin.math.floor
sealed class ChargepointListItem
/**
* A whole charging site (potentially with multiple chargepoints).
*
* @param timeRetrieved Time when this information was retrieved from the data source
* @param isDetailed Whether this data includes all available details (for many data sources,
* API calls that return a list may only give a compact representation)
*/
@Entity(primaryKeys = ["id", "dataSource"])
@Parcelize
data class ChargeLocation(
@@ -49,7 +57,9 @@ data class ChargeLocation(
@Embedded val openinghours: OpeningHours?,
@Embedded val cost: Cost?,
val license: String?,
@Embedded(prefix = "chargeprice") val chargepriceData: ChargepriceData?
@Embedded(prefix = "chargeprice") val chargepriceData: ChargepriceData?,
val timeRetrieved: Instant,
val isDetailed: Boolean
) : ChargepointListItem(), Equatable, Parcelable {
/**
* maximum power available from this charger.

View File

@@ -0,0 +1,28 @@
package net.vonforst.evmap.model
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
@Entity(
foreignKeys = [
ForeignKey(
entity = ChargeLocation::class,
parentColumns = arrayOf("id", "dataSource"),
childColumns = arrayOf("chargerId", "chargerDataSource"),
onDelete = ForeignKey.RESTRICT,
)
]
)
data class Favorite(
@PrimaryKey(autoGenerate = true)
val favoriteId: Long = 0,
val chargerId: Long,
val chargerDataSource: String
)
data class FavoriteWithDetail(
@Embedded() val favorite: Favorite,
@Embedded val charger: ChargeLocation
)

View File

@@ -20,6 +20,7 @@ import net.vonforst.evmap.model.*
@Database(
entities = [
ChargeLocation::class,
Favorite::class,
BooleanFilterValue::class,
MultipleChoiceFilterValue::class,
SliderFilterValue::class,
@@ -31,11 +32,12 @@ import net.vonforst.evmap.model.*
OCMConnectionType::class,
OCMCountry::class,
OCMOperator::class
], version = 14
], version = 16
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun chargeLocationsDao(): ChargeLocationsDao
abstract fun favoritesDao(): FavoritesDao
abstract fun filterValueDao(): FilterValueDao
abstract fun filterProfileDao(): FilterProfileDao
abstract fun recentAutocompletePlaceDao(): RecentAutocompletePlaceDao
@@ -53,7 +55,7 @@ abstract class AppDatabase : RoomDatabase() {
.addMigrations(
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_12, MIGRATION_13, MIGRATION_14, MIGRATION_15, MIGRATION_16
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -312,5 +314,37 @@ abstract class AppDatabase : RoomDatabase() {
}
}
private val MIGRATION_15 = object : Migration(14, 15) {
@SuppressLint("Range")
override fun migrate(db: SupportSQLiteDatabase) {
try {
db.beginTransaction()
db.execSQL("CREATE TABLE IF NOT EXISTS `Favorite` (`favoriteId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `chargerId` INTEGER NOT NULL, `chargerDataSource` TEXT NOT NULL, FOREIGN KEY(`chargerId`, `chargerDataSource`) REFERENCES `ChargeLocation`(`id`, `dataSource`) ON UPDATE NO ACTION ON DELETE RESTRICT )");
val cursor = db.query("SELECT * FROM `ChargeLocation`")
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndex("id"))
val dataSource = cursor.getString(cursor.getColumnIndex("dataSource"))
val values = ContentValues().apply {
put("chargerId", id)
put("chargerDataSource", dataSource)
}
db.insert("favorite", SQLiteDatabase.CONFLICT_ROLLBACK, values)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}
private val MIGRATION_16 = object : Migration(15, 16) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE `ChargeLocation` ADD `timeRetrieved` INTEGER NOT NULL DEFAULT 0")
db.execSQL("ALTER TABLE `ChargeLocation` ADD `isDetailed` INTEGER NOT NULL DEFAULT 0")
}
}
}
}

View File

@@ -0,0 +1,29 @@
package net.vonforst.evmap.storage
import androidx.lifecycle.LiveData
import androidx.room.*
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.model.FavoriteWithDetail
@Dao
interface FavoritesDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg favorites: Favorite)
@Delete
suspend fun delete(vararg favorites: Favorite)
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id")
fun getAllFavorites(): LiveData<List<FavoriteWithDetail>>
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id")
suspend fun getAllFavoritesAsync(): List<FavoriteWithDetail>
@Query("SELECT * FROM favorite LEFT JOIN chargelocation ON favorite.chargerDataSource = chargelocation.dataSource AND favorite.chargerId = chargelocation.id WHERE lat >= :lat1 AND lat <= :lat2 AND lng >= :lng1 AND lng <= :lng2")
suspend fun getFavoritesInBoundsAsync(
lat1: Double,
lat2: Double,
lng1: Double,
lng2: Double
): List<FavoriteWithDetail>
}

View File

@@ -11,6 +11,8 @@ 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.model.ChargeLocation
import net.vonforst.evmap.model.Favorite
import net.vonforst.evmap.model.FavoriteWithDetail
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.utils.distanceBetween
@@ -18,8 +20,8 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
AndroidViewModel(application) {
private var db = AppDatabase.getInstance(application)
val favorites: LiveData<List<ChargeLocation>> by lazy {
db.chargeLocationsDao().getAllChargeLocations()
val favorites: LiveData<List<FavoriteWithDetail>> by lazy {
db.favoritesDao().getAllFavorites()
}
val location: MutableLiveData<LatLng> by lazy {
@@ -28,8 +30,9 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
val availability: MediatorLiveData<Map<Long, Resource<ChargeLocationStatus>>> by lazy {
MediatorLiveData<Map<Long, Resource<ChargeLocationStatus>>>().apply {
addSource(favorites) { chargers ->
if (chargers != null) {
addSource(favorites) { favorites ->
if (favorites != null) {
val chargers = favorites.map { it.charger }
viewModelScope.launch {
val data = hashMapOf<Long, Resource<ChargeLocationStatus>>()
chargers.forEach { charger ->
@@ -54,9 +57,10 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
val listData: MediatorLiveData<List<FavoritesListItem>> by lazy {
MediatorLiveData<List<FavoritesListItem>>().apply {
val callback = { _: Any ->
listData.value = favorites.value?.map { charger ->
listData.value = favorites.value?.map { favorite ->
val charger = favorite.charger
FavoritesListItem(
charger,
favorite,
totalAvailable(charger.id),
charger.chargepoints.sumBy { it.count },
location.value.let { loc ->
@@ -78,11 +82,14 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
}
data class FavoritesListItem(
val charger: ChargeLocation,
val fav: FavoriteWithDetail,
val available: Resource<List<ChargepointStatus>>,
val total: Int,
val distance: Double?
) : Equatable
) : Equatable {
val charger
get() = fav.charger
}
private fun totalAvailable(id: Long): Resource<List<ChargepointStatus>> {
val availability = availability.value?.get(id) ?: return Resource.error(null, null)
@@ -97,12 +104,14 @@ class FavoritesViewModel(application: Application, geApiKey: String) :
fun insertFavorite(charger: ChargeLocation) {
viewModelScope.launch {
db.chargeLocationsDao().insert(charger)
db.favoritesDao()
.insert(Favorite(chargerId = charger.id, chargerDataSource = charger.dataSource))
}
}
fun deleteFavorite(charger: ChargeLocation) {
fun deleteFavorite(fav: Favorite) {
viewModelScope.launch {
db.chargeLocationsDao().delete(charger)
db.favoritesDao().delete(fav)
}
}
}

View File

@@ -222,8 +222,8 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
}
}
val favorites: LiveData<List<ChargeLocation>> by lazy {
db.chargeLocationsDao().getAllChargeLocations()
val favorites: LiveData<List<FavoriteWithDetail>> by lazy {
db.favoritesDao().getAllFavorites()
}
val searchResult: MutableLiveData<PlaceWithBounds> by lazy {
@@ -279,12 +279,14 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
fun insertFavorite(charger: ChargeLocation) {
viewModelScope.launch {
db.chargeLocationsDao().insert(charger)
db.favoritesDao()
.insert(Favorite(chargerId = charger.id, chargerDataSource = charger.dataSource))
}
}
fun deleteFavorite(charger: ChargeLocation) {
fun deleteFavorite(favorite: Favorite) {
viewModelScope.launch {
db.chargeLocationsDao().delete(charger)
db.favoritesDao().delete(favorite)
}
}
@@ -310,12 +312,12 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
if (filterStatus.value == FILTERS_FAVORITES) {
// load favorites from local DB
val b = mapPosition.bounds
var chargers = db.chargeLocationsDao().getChargeLocationsInBoundsAsync(
var chargers = db.favoritesDao().getFavoritesInBoundsAsync(
b.southwest.latitude,
b.northeast.latitude,
b.southwest.longitude,
b.northeast.longitude
) as List<ChargepointListItem>
).map { it.charger } as List<ChargepointListItem>
val clusterDistance = getClusterDistance(mapPosition.zoom)
clusterDistance?.let {