Compare commits

...

13 Commits
0.8.4 ... 0.9.0

Author SHA1 Message Date
johan12345
c2292ad7fa Release 0.9.0 2021-08-09 19:01:35 +02:00
Johan von Forstner
d5e29a5112 Android Auto: implement filter profiles
fixes #72
2021-08-09 19:00:20 +02:00
johan12345
77f478c9e0 fix loading donation products 2021-08-09 18:37:18 +02:00
johan12345
1008a2c2cd update Google donation percentage 2021-08-09 18:29:29 +02:00
Johan von Forstner
2219e2fe27 Show current filter profile title in filter edit view 2021-08-08 16:42:35 +02:00
Johan von Forstner
8ce145a9af add dataSource column to ChargeLocation table 2021-08-08 16:24:18 +02:00
Johan von Forstner
b799dae28b add DB migration for GE plug types
fixes problem with loading the availability of favorites
2021-08-08 16:01:05 +02:00
Johan von Forstner
07a482a6b6 fix lint error 2021-08-07 20:53:34 +02:00
Johan von Forstner
4f1253b201 upgrade Travis CI 2021-08-07 20:32:13 +02:00
Johan von Forstner
8bc4a7ae40 Mapbox Autocomplete: use proximity and locale 2021-08-07 20:22:38 +02:00
Johan von Forstner
d686becfe4 update Gradle and Android plugin 2021-08-07 19:54:34 +02:00
Johan von Forstner
a686c51b32 always use Mapbox autocomplete
to avoid Google API costs
fixes #105
2021-08-07 19:47:06 +02:00
Johan von Forstner
382ead9e08 update app description 2021-07-31 20:26:18 +02:00
28 changed files with 376 additions and 237 deletions

View File

@@ -1,5 +1,5 @@
language: java
dist: trusty
dist: focal
env:
global:
- secure: KYdFlMarsyXw+OHht1Atp+Kirbw9O09Ck14EjFuKb1eNtknurZ/tGEXuD+8xWh1W8W21kgHEG7s3rzru53t29buz+FW9f+ZmhEWXFP3OydyvXLw4BAVVOjm6xG2uHX/8MOGLJNM7cfaF25EPQ+kznHe84R29KaLH90mNRr2lPa4VnfbcnvDStiVaez/vJ72UoYSP5HICAzoF70yC3ZvvCK1hZv71UIysCbFE2IkxvMhG9OOGebdnRmFssaRCrvfRLjitobcLzkPWzZZIqdjNASf8/iAxX8VgGBYfVj8ID06AfMrtgXNJRCvcD0LICraQ+WPUbikMunRieGO8PNHSB5vKdPoC50aLUa0RoRb4G3QM1pR2A8xAFlIJFX2R7iY+2t24L9hRFqB98+QoQzutfkAI1T0rzem/wtpZpuan+bDawDJEHGCeYbE0aPDAl6lytgrEE9fRgV3c1jJLQzu0xIWG8YLl3iMg0hL+c0wCKXoeqrfCFS6kYmmG7W+rQp4tCZifvRbWfAXwIPQieffKxqdEuUwiUsYxdzCu9v9uU3nflEOLLuRgeMP3gV8mpur9b5GztpkfgfzcAqsF+NiY01kYgGtrgCYlMy0TxASE+UuALrtkQtU01wwhs9RH7Az0Ib3C+MT5DTjxHQCYETIViocmNEG2vfAbgHazCpGAhcY=

View File

@@ -15,10 +15,11 @@ Features
- [Material Design](https://material.io/)
- Shows all charging stations from the community-maintained [GoingElectric.de](https://www.goingelectric.de/stromtankstellen/) and [Open Charge Map](https://openchargemap.org) directories
- Realtime availability information (beta)
- Search places
- Realtime availability information (only in Europe)
- Search for places
- Advanced filtering options, including saved filter profiles
- Favorites list, also with availability information
- Charging price comparison, powered by [Chargeprice.app](https://chargeprice.app)
- Integrated price comparison using Chargeprice.app [Chargeprice.app](https://chargeprice.app) (only in Europe)
- Android Auto integration
- No ads, fully open source
- Compatible with Android 5.0 and above

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 30
versionCode 52
versionName "0.8.4"
versionCode 53
versionName "0.9.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -21,20 +21,16 @@ import net.vonforst.evmap.*
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.nameForPlugType
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.ReferenceData
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.GEReferenceDataRepository
import net.vonforst.evmap.storage.OCMReferenceDataRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.ChargerIconGenerator
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.viewmodel.Status
import net.vonforst.evmap.viewmodel.getReferenceData
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
@@ -50,9 +46,16 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
private val api by lazy {
createApi(prefs.dataSource, ctx)
}
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64)
init {
referenceData.observe(this) {
loadCharger()
}
}
override fun onGetTemplate(): Template {
if (charger == null) loadCharger()
@@ -181,8 +184,9 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
}
private fun loadCharger() {
val referenceData = referenceData.value ?: return
lifecycleScope.launch {
val response = api.getChargepointDetail(getReferenceData(), chargerSparse.id)
val response = api.getChargepointDetail(referenceData, chargerSparse.id)
if (response.status == Status.SUCCESS) {
charger = response.data!!
@@ -206,29 +210,4 @@ class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) :
}
}
}
private suspend fun getReferenceData(): ReferenceData {
val api = api
return when (api) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
api,
lifecycleScope,
db.geReferenceDataDao(),
prefs
).getReferenceData().await()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
api,
lifecycleScope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData().await()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}
}
}

View File

@@ -0,0 +1,90 @@
package net.vonforst.evmap.auto
import android.graphics.Bitmap
import androidx.car.app.CarContext
import androidx.car.app.Screen
import androidx.car.app.model.*
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.LiveData
import net.vonforst.evmap.R
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
import kotlin.math.roundToInt
class FilterScreen(ctx: CarContext) : Screen(ctx) {
private val prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(ctx)
val filterProfiles: LiveData<List<FilterProfile>> by lazy {
db.filterProfileDao().getProfiles(prefs.dataSource)
}
private val maxRows = 6
private val checkIcon =
CarIcon.Builder(IconCompat.createWithResource(carContext, R.drawable.ic_check)).build()
private val emptyIcon: CarIcon
init {
val size = (ctx.resources.displayMetrics.density * 24).roundToInt()
emptyIcon = CarIcon.Builder(
IconCompat.createWithBitmap(
Bitmap.createBitmap(
size,
size,
Bitmap.Config.ARGB_8888
)
)
).build()
}
init {
filterProfiles.observe(this) {
invalidate()
}
}
override fun onGetTemplate(): Template {
return ListTemplate.Builder().apply {
filterProfiles.value?.let {
setSingleList(buildFilterProfilesList(it.take(maxRows), prefs.filterStatus))
} ?: setLoading(true)
setTitle(carContext.getString(R.string.menu_filter))
setHeaderAction(Action.BACK)
}.build()
}
private fun buildFilterProfilesList(
profiles: List<FilterProfile>,
filterStatus: Long
): ItemList {
return ItemList.Builder().apply {
addItem(Row.Builder().apply {
setTitle(carContext.getString(R.string.no_filters))
if (FILTERS_DISABLED == filterStatus) {
setImage(checkIcon)
} else {
setImage(emptyIcon)
}
setOnClickListener {
prefs.filterStatus = FILTERS_DISABLED
screenManager.pop()
}
}.build())
profiles.forEach {
addItem(Row.Builder().apply {
setTitle(it.name)
if (it.id == filterStatus) {
setImage(checkIcon)
} else {
setImage(emptyIcon)
}
setOnClickListener {
prefs.filterStatus = it.id
screenManager.pop()
}
}.build())
}
setNoItemsMessage(carContext.getString(R.string.filterprofiles_empty_state))
}.build()
}
}

View File

@@ -8,6 +8,8 @@ import androidx.car.app.CarToast
import androidx.car.app.Screen
import androidx.car.app.model.*
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.IconCompat
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import com.car2go.maps.model.LatLng
import kotlinx.coroutines.*
@@ -15,21 +17,23 @@ import net.vonforst.evmap.R
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.await
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.ChargeLocation
import net.vonforst.evmap.model.ReferenceData
import net.vonforst.evmap.model.FILTERS_CUSTOM
import net.vonforst.evmap.model.FILTERS_DISABLED
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.GEReferenceDataRepository
import net.vonforst.evmap.storage.OCMReferenceDataRepository
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.ui.availabilityText
import net.vonforst.evmap.ui.getMarkerTint
import net.vonforst.evmap.utils.distanceBetween
import net.vonforst.evmap.viewmodel.filtersWithValue
import net.vonforst.evmap.viewmodel.getFilterValues
import net.vonforst.evmap.viewmodel.getFilters
import net.vonforst.evmap.viewmodel.getReferenceData
import java.io.IOException
import java.time.Duration
import java.time.ZonedDateTime
import kotlin.collections.set
import kotlin.math.roundToInt
/**
@@ -46,6 +50,7 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
private var lastUpdateLocation: Location? = null
private var chargers: List<ChargeLocation>? = null
private var prefs = PreferenceDataSource(ctx)
private val db = AppDatabase.getInstance(carContext)
private val api by lazy {
createApi(prefs.dataSource, ctx)
}
@@ -56,6 +61,20 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
HashMap()
private val maxRows = 6
private val referenceData = api.getReferenceData(lifecycleScope, carContext)
private val filterStatus = MutableLiveData<Long>().apply {
value = prefs.filterStatus.takeUnless { it == FILTERS_CUSTOM } ?: FILTERS_DISABLED
}
private val filterValues = db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val filters = api.getFilters(referenceData, carContext.stringProvider())
private val filtersWithValue = filtersWithValue(filters, filterValues)
init {
filtersWithValue.observe(this) {
loadChargers()
}
}
override fun onGetTemplate(): Template {
session.mapScreen = this
return PlaceListMapTemplate.Builder().apply {
@@ -91,6 +110,36 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
} ?: setLoading(true)
setCurrentLocationEnabled(true)
setHeaderAction(Action.BACK)
if (!favorites) {
val filtersCount = filtersWithValue.value?.count {
!it.value.hasSameValueAs(it.filter.defaultValue())
}
setActionStrip(
ActionStrip.Builder()
.addAction(
Action.Builder()
.setIcon(
CarIcon.Builder(
IconCompat.createWithResource(
carContext,
R.drawable.ic_filter
)
)
.setTint(if (filtersCount != null && filtersCount > 0) CarColor.SECONDARY else CarColor.DEFAULT)
.build()
)
.setOnClickListener {
screenManager.pushForResult(FilterScreen(carContext)) {
chargers = null
numUpdates = 0
filterStatus.value = prefs.filterStatus
}
session.mapScreen = null
}
.build())
.build())
}
build()
}.build()
}
@@ -178,13 +227,15 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
) {
lastUpdateLocation = location
// update displayed chargers
loadChargers(location)
loadChargers()
}
}
private val db = AppDatabase.getInstance(carContext)
private fun loadChargers() {
val location = location ?: return
val referenceData = referenceData.value ?: return
val filters = filtersWithValue.value ?: return
private fun loadChargers(location: Location) {
numUpdates++
println(numUpdates)
if (numUpdates > maxNumUpdates) {
@@ -204,22 +255,22 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
}
} else {
val response = api.getChargepointsRadius(
getReferenceData(),
referenceData,
LatLng.fromLocation(location),
searchRadius,
zoom = 16f,
null
filters
)
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
chargers?.let {
if (it.size < 6) {
// try again with larger radius
val response = api.getChargepointsRadius(
getReferenceData(),
referenceData,
LatLng.fromLocation(location),
searchRadius * 5,
searchRadius * 10,
zoom = 16f,
emptyList()
filters
)
chargers =
response.data?.filterIsInstance(ChargeLocation::class.java)
@@ -259,29 +310,4 @@ class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boole
}
}
}
private suspend fun getReferenceData(): ReferenceData {
val api = api
return when (api) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
api,
lifecycleScope,
db.geReferenceDataDao(),
prefs
).getReferenceData().await()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
api,
lifecycleScope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData().await()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}
}
}

View File

@@ -1,33 +0,0 @@
package net.vonforst.evmap.autocomplete
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.inputmethod.InputMethodManager
import androidx.fragment.app.Fragment
import com.car2go.maps.google.adapter.AnyMapAdapter
import com.google.android.libraries.places.api.model.Place
import com.google.android.libraries.places.widget.Autocomplete
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
import net.vonforst.evmap.fragment.REQUEST_AUTOCOMPLETE
import net.vonforst.evmap.viewmodel.PlaceWithBounds
fun launchAutocomplete(fragment: Fragment) {
val fields = listOf(Place.Field.LAT_LNG, Place.Field.VIEWPORT)
val intent: Intent = Autocomplete.IntentBuilder(
AutocompleteActivityMode.OVERLAY, fields
)
.build(fragment.requireActivity())
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
fragment.startActivityForResult(intent, REQUEST_AUTOCOMPLETE)
// show keyboard
val imm = fragment.requireContext()
.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(0, 0)
}
fun handleAutocompleteResult(intent: Intent): PlaceWithBounds? {
val place = Autocomplete.getPlaceFromIntent(intent)
return PlaceWithBounds(AnyMapAdapter.adapt(place.latLng), AnyMapAdapter.adapt(place.viewport))
}

View File

@@ -54,6 +54,10 @@ class DonateFragment : Fragment() {
layoutManager = LinearLayoutManager(context)
}
vm.products.observe(viewLifecycleOwner) {
print(it)
}
vm.purchaseSuccessful.observe(viewLifecycleOwner, Observer {
Snackbar.make(view, R.string.donation_successful, Snackbar.LENGTH_LONG).show()
})

View File

@@ -54,12 +54,12 @@ class DonateViewModel(application: Application) : AndroidViewModel(application),
.build()
billingClient.querySkuDetailsAsync(params) { result, details ->
if (result.responseCode == BillingClient.BillingResponseCode.OK && details != null) {
products.value = Resource.success(details
products.postValue(Resource.success(details
.sortedBy { it.priceAmountMicros }
.map { DonationItem(it) }
)
))
} else {
products.value = Resource.error(result.debugMessage, null)
products.postValue(Resource.error(result.debugMessage, null))
}
}
}

View File

@@ -51,7 +51,7 @@
android:id="@+id/products_list"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="8dp"
android:layout_marginTop="16dp"
app:data="@{vm.products.data}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View File

@@ -4,7 +4,7 @@
<item>Google Maps</item>
<item>OpenStreetMap (Mapbox)</item>
</string-array>
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 30% Gebühren ab.</string>
<string name="donations_info" formatted="false">Findest du EVMap nützlich? Unterstütze die Weiterentwicklung der App mit einer Spende an den Entwickler.\n\nGoogle zieht von der Spende 15% Gebühren ab.</string>
<string name="auto_location_service">EVMap läuft unter Android Auto und nutzt dafür deinen Standort.</string>
<string name="auto_no_chargers_found">Keine Ladestationen in der Nähe gefunden</string>
<string name="auto_no_favorites_found">Keine Favoriten gefunden</string>

View File

@@ -9,7 +9,7 @@
<item>mapbox</item>
</string-array>
<string name="pref_map_provider_default" translatable="false">google</string>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 30% off every donation.</string>
<string name="donations_info" formatted="false">Do you find EVMap useful? Support its development by sending a donation to the developer.\n\nGoogle takes 15% off every donation.</string>
<string name="auto_location_service">EVMap is running on Android Auto and using your location.</string>
<string name="auto_no_chargers_found">No nearby chargers found</string>
<string name="auto_no_favorites_found">No favorites found</string>

View File

@@ -168,7 +168,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
}
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
if (checked) {
checkedItem = position
checkedItem = holder.bindingAdapterPosition
root.post {
notifyDataSetChanged()
}

View File

@@ -56,6 +56,7 @@ data class GEChargeLocation(
) : GEChargepointListItem() {
override fun convert(apikey: String) = ChargeLocation(
id,
"goingelectric",
name,
coordinates.convert(),
address.convert(),

View File

@@ -46,6 +46,7 @@ data class OCMChargepoint(
) {
fun convert(refData: OCMReferenceData) = ChargeLocation(
id,
"openchargemap",
addressInfo.title,
Coordinate(addressInfo.latitude, addressInfo.longitude),
addressInfo.toAddress(refData),

View File

@@ -3,6 +3,7 @@ package net.vonforst.evmap.autocomplete
import android.content.Context
import android.content.Intent
import android.view.inputmethod.InputMethodManager
import androidx.core.os.ConfigurationCompat
import androidx.fragment.app.Fragment
import com.car2go.maps.model.LatLng
import com.car2go.maps.model.LatLngBounds
@@ -15,9 +16,13 @@ import net.vonforst.evmap.fragment.REQUEST_AUTOCOMPLETE
import net.vonforst.evmap.viewmodel.PlaceWithBounds
fun launchAutocomplete(fragment: Fragment) {
val placeOptions = PlaceOptions.builder()
.build(PlaceOptions.MODE_CARDS)
fun launchAutocomplete(fragment: Fragment, location: LatLng?) {
val placeOptions = PlaceOptions.builder().apply {
location?.let {
proximity(Point.fromLngLat(location.longitude, location.latitude))
}
language(ConfigurationCompat.getLocales(fragment.resources.configuration)[0].language)
}.build(PlaceOptions.MODE_CARDS)
val intent = PlaceAutocomplete.IntentBuilder()
.accessToken(fragment.getString(R.string.mapbox_key))

View File

@@ -51,6 +51,12 @@ class FilterFragment : Fragment() {
(requireActivity() as MapsActivity).appBarConfiguration
)
vm.filterProfile.observe(viewLifecycleOwner) {
if (it != null) {
toolbar.title = "${getString(R.string.menu_filter)}: ${it.name}"
}
}
binding.filtersList.apply {
adapter = FiltersAdapter()
layoutManager =

View File

@@ -297,7 +297,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
}
binding.search.setOnClickListener {
launchAutocomplete(this)
launchAutocomplete(this, vm.location.value)
}
binding.detailAppBar.toolbar.setNavigationOnClickListener {
bottomSheetBehavior.state = STATE_COLLAPSED

View File

@@ -24,10 +24,11 @@ import kotlin.math.floor
sealed class ChargepointListItem
@Entity
@Entity(primaryKeys = ["id", "dataSource"])
@Parcelize
data class ChargeLocation(
@PrimaryKey val id: Long,
val id: Long,
val dataSource: String,
val name: String,
@Embedded val coordinates: Coordinate,
@Embedded val address: Address,

View File

@@ -9,6 +9,9 @@ interface ChargeLocationsDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg locations: ChargeLocation)
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertBlocking(vararg locations: ChargeLocation)
@Delete
suspend fun delete(vararg locations: ChargeLocation)
@@ -17,4 +20,7 @@ interface ChargeLocationsDao {
@Query("SELECT * FROM chargelocation")
suspend fun getAllChargeLocationsAsync(): List<ChargeLocation>
@Query("SELECT * FROM chargelocation")
fun getAllChargeLocationsBlocking(): List<ChargeLocation>
}

View File

@@ -1,6 +1,8 @@
package net.vonforst.evmap.storage
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@@ -8,6 +10,7 @@ import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
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
@@ -26,7 +29,7 @@ import net.vonforst.evmap.model.*
OCMConnectionType::class,
OCMCountry::class,
OCMOperator::class
], version = 12
], version = 13
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
@@ -47,7 +50,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_12, MIGRATION_13
)
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
@@ -251,5 +254,52 @@ abstract class AppDatabase : RoomDatabase() {
}
}
}
private val MIGRATION_13 = object : Migration(12, 13) {
override fun migrate(db: SupportSQLiteDatabase) {
db.beginTransaction()
try {
// add column dataSource to ChargeLocation table
db.execSQL("ALTER TABLE `ChargeLocation` ADD `dataSource` TEXT NOT NULL DEFAULT 'openchargemap'")
// this should have been included in MIGRATION_12:
// Update GoingElectric format of plug types for favorites to generic EVMap format
val cursor = db.query("SELECT * FROM `ChargeLocation`")
while (cursor.moveToNext()) {
val chargepoints =
Converters().toChargepointList(cursor.getString(cursor.getColumnIndex("chargepoints")))!!
val updated = chargepoints.map {
it.copy(type = GEChargepoint.convertTypeFromGE(it.type))
}
if (updated != chargepoints) {
db.update(
"ChargeLocation",
SQLiteDatabase.CONFLICT_ROLLBACK,
ContentValues().apply {
put("chargepoints", Converters().fromChargepointList(updated))
put("dataSource", "goingelectric")
},
"id = ?",
arrayOf(cursor.getLong(cursor.getColumnIndex("id")))
)
}
}
// update ChargeLocation table to change primary key
db.execSQL(
"CREATE TABLE `ChargeLocationNew` (`id` INTEGER NOT NULL, `dataSource` TEXT NOT NULL, `name` TEXT NOT NULL, `chargepoints` TEXT NOT NULL, `network` TEXT, `url` TEXT NOT NULL, `editUrl` TEXT, `verified` INTEGER NOT NULL, `barrierFree` INTEGER, `operator` TEXT, `generalInformation` TEXT, `amenities` TEXT, `locationDescription` TEXT, `photos` TEXT, `chargecards` TEXT, `license` TEXT, `lat` REAL NOT NULL, `lng` REAL NOT NULL, `city` TEXT, `country` TEXT, `postcode` TEXT, `street` TEXT, `fault_report_created` INTEGER, `fault_report_description` TEXT, `twentyfourSeven` INTEGER, `description` TEXT, `mostart` TEXT, `moend` TEXT, `tustart` TEXT, `tuend` TEXT, `westart` TEXT, `weend` TEXT, `thstart` TEXT, `thend` TEXT, `frstart` TEXT, `frend` TEXT, `sastart` TEXT, `saend` TEXT, `sustart` TEXT, `suend` TEXT, `hostart` TEXT, `hoend` TEXT, `freecharging` INTEGER, `freeparking` INTEGER, `descriptionShort` TEXT, `descriptionLong` TEXT, `chargepricecountry` TEXT, `chargepricenetwork` TEXT, `chargepriceplugTypes` TEXT, PRIMARY KEY(`id`, `dataSource`))"
);
val columnList =
"`id`,`dataSource`,`name`,`chargepoints`,`network`,`url`,`editUrl`,`verified`,`barrierFree`,`operator`,`generalInformation`,`amenities`,`locationDescription`,`photos`,`chargecards`,`license`,`lat`,`lng`,`city`,`country`,`postcode`,`street`,`fault_report_created`,`fault_report_description`,`twentyfourSeven`,`description`,`mostart`,`moend`,`tustart`,`tuend`,`westart`,`weend`,`thstart`,`thend`,`frstart`,`frend`,`sastart`,`saend`,`sustart`,`suend`,`hostart`,`hoend`,`freecharging`,`freeparking`,`descriptionShort`,`descriptionLong`,`chargepricecountry`,`chargepricenetwork`,`chargepriceplugTypes`"
db.execSQL("INSERT INTO `ChargeLocationNew`($columnList) SELECT $columnList FROM `ChargeLocation`")
db.execSQL("DROP TABLE `ChargeLocation`")
db.execSQL("ALTER TABLE `ChargeLocationNew` RENAME TO `ChargeLocation`")
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
}
}
}

View File

@@ -0,0 +1,81 @@
package net.vonforst.evmap.viewmodel
import android.content.Context
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import kotlinx.coroutines.CoroutineScope
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.StringProvider
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.*
import kotlin.reflect.full.cast
fun ChargepointApi<ReferenceData>.getReferenceData(
scope: CoroutineScope,
ctx: Context
): LiveData<out ReferenceData> {
val db = AppDatabase.getInstance(ctx)
val prefs = PreferenceDataSource(ctx)
return when (this) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
this,
scope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
this,
scope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}
}
fun filtersWithValue(
filters: LiveData<List<Filter<FilterValue>>>,
filterValues: LiveData<List<FilterValue>>
): MediatorLiveData<FilterValues> =
MediatorLiveData<FilterValues>().apply {
listOf(filters, filterValues).forEach {
addSource(it) {
val f = filters.value ?: return@addSource
val values = filterValues.value ?: return@addSource
value = f.map { filter ->
val value =
values.find { it.key == filter.key } ?: filter.defaultValue()
FilterWithValue(filter, filter.valueClass.cast(value))
}
}
}
}
fun ChargepointApi<ReferenceData>.getFilters(
referenceData: LiveData<out ReferenceData>,
stringProvider: StringProvider
) = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { data ->
value = getFilters(data, stringProvider)
}
}
fun FilterValueDao.getFilterValues(filterStatus: LiveData<Long>, dataSource: String) =
MediatorLiveData<List<FilterValue>>().apply {
var source: LiveData<List<FilterValue>>? = null
addSource(filterStatus) { status ->
source?.let { removeSource(it) }
source = getFilterValues(status, dataSource)
addSource(source!!) { result ->
value = result
}
}
}

View File

@@ -5,60 +5,19 @@ import androidx.lifecycle.*
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.ChargepointApi
import net.vonforst.evmap.api.createApi
import net.vonforst.evmap.api.goingelectric.GoingElectricApiWrapper
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.*
import kotlin.reflect.full.cast
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
internal fun filtersWithValue(
filters: LiveData<List<Filter<FilterValue>>>,
filterValues: LiveData<List<FilterValue>>
): MediatorLiveData<FilterValues> =
MediatorLiveData<FilterValues>().apply {
listOf(filters, filterValues).forEach {
addSource(it) {
val f = filters.value ?: return@addSource
val values = filterValues.value ?: return@addSource
value = f.map { filter ->
val value =
values.find { it.key == filter.key } ?: filter.defaultValue()
FilterWithValue(filter, filter.valueClass.cast(value))
}
}
}
}
class FilterViewModel(application: Application) : AndroidViewModel(application) {
private var db = AppDatabase.getInstance(application)
private var prefs = PreferenceDataSource(application)
private var api: ChargepointApi<ReferenceData> = createApi(prefs.dataSource, application)
private val referenceData: LiveData<out ReferenceData> by lazy {
val api = api
when (api) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
api,
viewModelScope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
api,
viewModelScope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}
}
private val referenceData = api.getReferenceData(viewModelScope, application)
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { data ->
value = api.getFilters(data, application.stringProvider())

View File

@@ -19,7 +19,9 @@ import net.vonforst.evmap.api.openchargemap.OCMReferenceData
import net.vonforst.evmap.api.openchargemap.OpenChargeMapApiWrapper
import net.vonforst.evmap.api.stringProvider
import net.vonforst.evmap.model.*
import net.vonforst.evmap.storage.*
import net.vonforst.evmap.storage.AppDatabase
import net.vonforst.evmap.storage.FilterProfile
import net.vonforst.evmap.storage.PreferenceDataSource
import net.vonforst.evmap.utils.distanceBetween
import java.io.IOException
@@ -54,48 +56,19 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
val mapPosition: MutableLiveData<MapPosition> by lazy {
MutableLiveData<MapPosition>()
}
private val filterValues: LiveData<List<FilterValue>> by lazy {
MediatorLiveData<List<FilterValue>>().apply {
var source: LiveData<List<FilterValue>>? = null
addSource(filterStatus) { status ->
source?.let { removeSource(it) }
source = db.filterValueDao().getFilterValues(status, prefs.dataSource)
addSource(source!!) { result ->
value = result
}
val filterStatus: MutableLiveData<Long> by lazy {
MutableLiveData<Long>().apply {
value = prefs.filterStatus
observeForever {
prefs.filterStatus = it
if (it != FILTERS_DISABLED) prefs.lastFilterProfile = it
}
}
}
private val referenceData: LiveData<out ReferenceData> by lazy {
val api = api
when (api) {
is GoingElectricApiWrapper -> {
GEReferenceDataRepository(
api,
viewModelScope,
db.geReferenceDataDao(),
prefs
).getReferenceData()
}
is OpenChargeMapApiWrapper -> {
OCMReferenceDataRepository(
api,
viewModelScope,
db.ocmReferenceDataDao(),
prefs
).getReferenceData()
}
else -> {
throw RuntimeException("no reference data implemented")
}
}
}
private val filters = MediatorLiveData<List<Filter<FilterValue>>>().apply {
addSource(referenceData) { data ->
val api = api
value = api.getFilters(data, application.stringProvider())
}
}
private val filterValues: LiveData<List<FilterValue>> =
db.filterValueDao().getFilterValues(filterStatus, prefs.dataSource)
private val referenceData = api.getReferenceData(viewModelScope, application)
private val filters = api.getFilters(referenceData, application.stringProvider())
private val filtersWithValue: LiveData<FilterValues> by lazy {
filtersWithValue(filters, filterValues)
@@ -271,16 +244,6 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
}
}
val filterStatus: MutableLiveData<Long> by lazy {
MutableLiveData<Long>().apply {
value = prefs.filterStatus
observeForever {
prefs.filterStatus = it
if (it != FILTERS_DISABLED) prefs.lastFilterProfile = it
}
}
}
fun reloadPrefs() {
filterStatus.value = prefs.filterStatus
}

View File

@@ -10,7 +10,7 @@ buildscript {
gradlePluginPortal()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.1'
classpath 'com.android.tools.build:gradle:7.0.0'
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"

View File

@@ -1,18 +1,17 @@
Mit EVMap kannst du Stromtankstellen in deiner Nähe komfortabel über dein Android-Smartphone finden. Die Datenbank von GoingElectric.de wird als Datenquelle genutzt und bietet Community-gepflegte Informationen zu mehr als 100.000 Ladepunkten an über 40.000 Standorten in 48 Ländern (die meisten in Europa). Für viele Ladepunkte kann zusätzlich der aktuelle Status (verfügbar oder belegt) angezeigt werden.
Mit EVMap kannst du Stromtankstellen in deiner Nähe komfortabel über dein Android-Smartphone finden. Die Datenbanken von GoingElectric.de oder Open Charge Map werden als Datenquelle genutzt und bieten Community-gepflegte Informationen zu Ladestationen auf der ganzen Welt. Für viele Ladepunkte in Europa kann zusätzlich der aktuelle Status (verfügbar oder belegt) angezeigt werden.
Funktionen:
- Zeitgemäßes Material Design
- Anzeige der Stromtankstellen aus dem GoingElectric-Stromtankstellenverzeichnis
- Echtzeit-Verfügbarkeitsanzeige für viele Ladesäulen
- Direkte Links zu Chargeprice.app für einen Preisvergleich für die jeweilige Ladesäule
- Markierung des aktuellen Standorts
- Google Maps-Verkehrsdaten
- Anzeige der Stromtankstellen aus den Stromtankstellenverzeichnissen von GoingElectric.de und Open Charge Map
- Echtzeit-Verfügbarkeitsanzeige für viele Ladesäulen (nur in Europa)
- Integrierter Preisvergleich für die jeweilige Ladesäule mit Chargeprice.app (nur in Europa)
- Google Maps oder OpenStreetMap (Mapbox) können für die Kartendaten genutzt werden
- Suche nach Orten
- Erweiterte Filterfunktionen
- Erweiterte Filterfunktionen, Filterprofile speichern
- Favoritenliste, auch mit Anzeige der Verfügbarkeit
- Unterstützung für Android Auto
- Keine nervige Werbung
EVMap ist ein Open-Source-Projekt und unter https://github.com/johan12345/EVMap zu finden.
Die App ist kein offizielles Angebot von GoingElectric.de, sondern nutzt die öffentliche API dieser Seite.
Die App ist kein offizielles Angebot von GoingElectric.de oder Open Charge Map, sondern nutzt die öffentlichen APIs dieser Seiten.

View File

@@ -1,17 +1,17 @@
Using EVMap, you can find electric vehicle chargers comfortably using your Android phone. It provides mobile access to the community-driven database from GoingElectric.de, containing more than 100,000 chargepoints at 40,000 locations in 48 countries (focusing on Europe). For many chargepoints, you can see real-time status information.
Using EVMap, you can find electric vehicle chargers comfortably using your Android phone. It provides mobile access to the community-driven databases from GoingElectric.de and Open Charge Map, containing information about charging locations across the world. For many chargepoints in Europe, you can see real-time status information.
Features:
- Material Design
- Shows all charging stations from the community-maintained GoingElectric.de directory
- Realtime availability information (beta)
- Direct link to Chargeprice.app for comparing prices
- Marker for current location
- Search places
- Shows all charging stations from the community-maintained GoingElectric.de and Open Charge Map directories
- Realtime availability information (only in Europe)
- Integrated price comparison using Chargeprice.app (only in Europe)
- Map data from Google Maps or OpenStreetMap (Mapbox)
- Search for places
- Advanced filtering options, including saved filter profiles
- Favorites list, also with availability information
- Advanced filtering options
- Android Auto support
- No ads, fully open source
EVMap is an open source project and can be found at https://github.com/johan12345/EVMap.
This app is not an official product of GoingElectric.de, but only uses its public API.
This app is not an official product of GoingElectric.de or Open Charge Map, it only uses their public APIs.

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip