mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-26 00:27:45 -05:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2292ad7fa | ||
|
|
d5e29a5112 | ||
|
|
77f478c9e0 | ||
|
|
1008a2c2cd | ||
|
|
2219e2fe27 | ||
|
|
8ce145a9af | ||
|
|
b799dae28b | ||
|
|
07a482a6b6 | ||
|
|
4f1253b201 | ||
|
|
8bc4a7ae40 | ||
|
|
d686becfe4 | ||
|
|
a686c51b32 | ||
|
|
382ead9e08 | ||
|
|
2da7ea4c05 | ||
|
|
20c4274c55 | ||
|
|
748212189f | ||
|
|
d86a49beb7 | ||
|
|
f8b1a20d1a | ||
|
|
14edb6f0cd | ||
|
|
7726088f91 | ||
|
|
cbc7c5a6d8 | ||
|
|
d510d81914 | ||
|
|
9f5abd6c91 | ||
|
|
966f62ac3d | ||
|
|
91caf40bdb | ||
|
|
72c0293365 | ||
|
|
ca9dc9629f | ||
|
|
438e529257 |
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 50
|
||||
versionName "0.8.2"
|
||||
versionCode 53
|
||||
versionName "0.9.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -139,7 +139,7 @@ dependencies {
|
||||
googleImplementation 'androidx.car.app:app:1.0.0'
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = '1f050d860f'
|
||||
def anyMapsVersion = '95ddd6c083'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
|
||||
|
||||
@@ -4,62 +4,24 @@ import android.Manifest
|
||||
import android.content.*
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.location.Location
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.IBinder
|
||||
import android.os.ResultReceiver
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.Session
|
||||
import androidx.car.app.model.*
|
||||
import androidx.car.app.model.Distance.UNIT_KILOMETERS
|
||||
import androidx.car.app.validation.HostValidator
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.*
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import com.car2go.maps.model.LatLng
|
||||
import kotlinx.coroutines.*
|
||||
import net.vonforst.evmap.*
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.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.utils.distanceBetween
|
||||
import java.io.IOException
|
||||
import java.time.Duration
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
interface LocationAwareScreen {
|
||||
fun updateLocation(location: Location)
|
||||
}
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class CarAppService : androidx.car.app.CarAppService() {
|
||||
override fun createHostValidator(): HostValidator {
|
||||
return if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) {
|
||||
@@ -76,6 +38,7 @@ class CarAppService : androidx.car.app.CarAppService() {
|
||||
}
|
||||
}
|
||||
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
var mapScreen: LocationAwareScreen? = null
|
||||
set(value) {
|
||||
@@ -158,566 +121,3 @@ class EVMapSession(val cas: CarAppService) : Session(), LifecycleObserver {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Welcome screen with selection between favorites and nearby chargers
|
||||
*/
|
||||
class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), LocationAwareScreen {
|
||||
private var location: Location? = null
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.app_name))
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
}
|
||||
setItemList(ItemList.Builder().apply {
|
||||
addItem(Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_chargers_closeby))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_address
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(MapScreen(carContext, session, favorites = false))
|
||||
}
|
||||
.build())
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_favorites))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_fav
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(MapScreen(carContext, session, favorites = true))
|
||||
}
|
||||
.build())
|
||||
}.build())
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Screen to grant location permission
|
||||
*/
|
||||
class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
override fun onGetTemplate(): Template {
|
||||
return MessageTemplate.Builder(carContext.getString(R.string.auto_location_permission_needed))
|
||||
.setTitle(carContext.getString(R.string.app_name))
|
||||
.setHeaderAction(Action.APP_ICON)
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.grant_on_phone))
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
val intent = Intent(carContext, PermissionActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(
|
||||
PermissionActivity.EXTRA_RESULT_RECEIVER,
|
||||
object : ResultReceiver(null) {
|
||||
override fun onReceiveResult(
|
||||
resultCode: Int,
|
||||
resultData: Bundle?
|
||||
) {
|
||||
if (resultData!!.getBoolean(PermissionActivity.RESULT_GRANTED)) {
|
||||
session.bindLocationService()
|
||||
screenManager.push(
|
||||
WelcomeScreen(
|
||||
carContext,
|
||||
session
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
})
|
||||
.build()
|
||||
)
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.cancel))
|
||||
.setOnClickListener {
|
||||
carContext.finishCarApp()
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main map screen showing either nearby chargers or favorites
|
||||
*/
|
||||
class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) :
|
||||
Screen(ctx), LocationAwareScreen {
|
||||
private var updateCoroutine: Job? = null
|
||||
private var numUpdates = 0
|
||||
private val maxNumUpdates = 3
|
||||
|
||||
private var location: Location? = null
|
||||
private var lastUpdateLocation: Location? = null
|
||||
private var chargers: List<ChargeLocation>? = null
|
||||
private var prefs = PreferenceDataSource(ctx)
|
||||
private val api by lazy {
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
private val searchRadius = 5 // kilometers
|
||||
private val updateThreshold = 2000 // meters
|
||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus>> =
|
||||
HashMap()
|
||||
private val maxRows = 6
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
if (favorites) {
|
||||
R.string.auto_favorites
|
||||
} else {
|
||||
R.string.auto_chargers_closeby
|
||||
}
|
||||
)
|
||||
)
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
} ?: setLoading(true)
|
||||
chargers?.take(maxRows)?.let { chargerList ->
|
||||
val builder = ItemList.Builder()
|
||||
chargerList.forEach { charger ->
|
||||
builder.addItem(formatCharger(charger))
|
||||
}
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(
|
||||
if (favorites) {
|
||||
R.string.auto_no_favorites_found
|
||||
} else {
|
||||
R.string.auto_no_chargers_found
|
||||
}
|
||||
)
|
||||
)
|
||||
setItemList(builder.build())
|
||||
} ?: setLoading(true)
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.BACK)
|
||||
build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun formatCharger(charger: ChargeLocation): Row {
|
||||
val color = ContextCompat.getColor(carContext, getMarkerTint(charger))
|
||||
val place =
|
||||
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.createCustom(color, color))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
return Row.Builder().apply {
|
||||
setTitle(charger.name)
|
||||
val text = SpannableStringBuilder()
|
||||
|
||||
// distance
|
||||
location?.let {
|
||||
val distance = distanceBetween(
|
||||
it.latitude, it.longitude,
|
||||
charger.coordinates.lat, charger.coordinates.lng
|
||||
) / 1000
|
||||
text.append(
|
||||
"distance",
|
||||
DistanceSpan.create(Distance.create(distance, UNIT_KILOMETERS)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
// power
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append("${charger.maxPower.roundToInt()} kW")
|
||||
|
||||
// availability
|
||||
availabilities[charger.id]?.second?.let { av ->
|
||||
val status = av.status.values.flatten()
|
||||
val available = availabilityText(status)
|
||||
val total = charger.chargepoints.sumBy { it.count }
|
||||
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append(
|
||||
"$available/$total",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
addText(text)
|
||||
setMetadata(
|
||||
Metadata.Builder()
|
||||
.setPlace(place)
|
||||
.build()
|
||||
)
|
||||
|
||||
setOnClickListener {
|
||||
screenManager.push(ChargerDetailScreen(carContext, charger))
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
if (updateCoroutine != null) {
|
||||
// don't update while still loading last update
|
||||
return
|
||||
}
|
||||
|
||||
invalidate()
|
||||
|
||||
if (lastUpdateLocation == null ||
|
||||
location.distanceTo(lastUpdateLocation) > updateThreshold
|
||||
) {
|
||||
lastUpdateLocation = location
|
||||
// update displayed chargers
|
||||
loadChargers(location)
|
||||
}
|
||||
}
|
||||
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
|
||||
private fun loadChargers(location: Location) {
|
||||
numUpdates++
|
||||
println(numUpdates)
|
||||
if (numUpdates > maxNumUpdates) {
|
||||
CarToast.makeText(carContext, R.string.auto_no_refresh_possible, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
updateCoroutine = lifecycleScope.launch {
|
||||
try {
|
||||
// load chargers
|
||||
if (favorites) {
|
||||
chargers = db.chargeLocationsDao().getAllChargeLocationsAsync().sortedBy {
|
||||
distanceBetween(
|
||||
location.latitude, location.longitude,
|
||||
it.coordinates.lat, it.coordinates.lng
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val response = api.getChargepointsRadius(
|
||||
getReferenceData(),
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius,
|
||||
zoom = 16f,
|
||||
null
|
||||
)
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
chargers?.let {
|
||||
if (it.size < 6) {
|
||||
// try again with larger radius
|
||||
val response = api.getChargepointsRadius(
|
||||
getReferenceData(),
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius * 5,
|
||||
zoom = 16f,
|
||||
emptyList()
|
||||
)
|
||||
chargers =
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove outdated availabilities
|
||||
availabilities = availabilities.filter {
|
||||
Duration.between(
|
||||
it.value.first,
|
||||
ZonedDateTime.now()
|
||||
) > availabilityUpdateThreshold
|
||||
}.toMutableMap()
|
||||
|
||||
// update availabilities
|
||||
chargers?.take(maxRows)?.map {
|
||||
lifecycleScope.async {
|
||||
// update only if not yet stored
|
||||
if (!availabilities.containsKey(it.id)) {
|
||||
val date = ZonedDateTime.now()
|
||||
val availability = getAvailability(it).data
|
||||
if (availability != null) {
|
||||
availabilities[it.id] = date to availability
|
||||
}
|
||||
}
|
||||
}
|
||||
}?.awaitAll()
|
||||
|
||||
updateCoroutine = null
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
|
||||
var charger: ChargeLocation? = null
|
||||
var photo: Bitmap? = null
|
||||
private var availability: ChargeLocationStatus? = null
|
||||
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
private val api by lazy {
|
||||
createApi(prefs.dataSource, ctx)
|
||||
}
|
||||
|
||||
private val iconGen = ChargerIconGenerator(carContext, null, oversize = 1.4f, height = 64)
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
if (charger == null) loadCharger()
|
||||
|
||||
return PaneTemplate.Builder(
|
||||
Pane.Builder().apply {
|
||||
charger?.let { charger ->
|
||||
addRow(Row.Builder().apply {
|
||||
setTitle(charger.address.toString())
|
||||
|
||||
val icon = iconGen.getBitmap(
|
||||
tint = getMarkerTint(charger),
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti()
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(icon)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
|
||||
val chargepointsText = SpannableStringBuilder()
|
||||
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||
if (i > 0) chargepointsText.append(" · ")
|
||||
chargepointsText.append(
|
||||
"${cp.count}× ${
|
||||
nameForPlugType(
|
||||
carContext.stringProvider(),
|
||||
cp.type
|
||||
)
|
||||
} ${cp.formatPower()}"
|
||||
)
|
||||
availability?.status?.get(cp)?.let { status ->
|
||||
chargepointsText.append(
|
||||
" (${availabilityText(status)}/${cp.count})",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
addText(chargepointsText)
|
||||
}.build())
|
||||
addRow(Row.Builder().apply {
|
||||
photo?.let {
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
}
|
||||
val operatorText = StringBuilder().apply {
|
||||
charger.operator?.let { append(it) }
|
||||
charger.network?.let {
|
||||
if (isNotEmpty()) append(" · ")
|
||||
append(it)
|
||||
}
|
||||
}.ifEmpty {
|
||||
carContext.getString(R.string.unknown_operator)
|
||||
}
|
||||
setTitle(operatorText)
|
||||
|
||||
charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) }
|
||||
charger.faultReport?.created?.let {
|
||||
addText(
|
||||
carContext.getString(
|
||||
R.string.auto_fault_report_date,
|
||||
it.atZone(ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/*val types = charger.chargepoints.map { it.type }.distinct()
|
||||
if (types.size == 1) {
|
||||
setImage(
|
||||
CarIcon.of(IconCompat.createWithResource(carContext, iconForPlugType(types[0]))),
|
||||
Row.IMAGE_TYPE_ICON)
|
||||
}*/
|
||||
}.build())
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_navigation
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.navigate))
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener {
|
||||
navigateToCharger(charger)
|
||||
}
|
||||
.build())
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.open_in_app))
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
val intent = Intent(carContext, MapsActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(EXTRA_CHARGER_ID, charger.id)
|
||||
.putExtra(EXTRA_LAT, charger.coordinates.lat)
|
||||
.putExtra(EXTRA_LON, charger.coordinates.lng)
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
})
|
||||
.build()
|
||||
)
|
||||
} ?: setLoading(true)
|
||||
}.build()
|
||||
).apply {
|
||||
setTitle(chargerSparse.name)
|
||||
setHeaderAction(Action.BACK)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun navigateToCharger(charger: ChargeLocation) {
|
||||
val coord = charger.coordinates
|
||||
val intent =
|
||||
Intent(
|
||||
CarContext.ACTION_NAVIGATE,
|
||||
Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
|
||||
)
|
||||
carContext.startCarApp(intent)
|
||||
}
|
||||
|
||||
private fun loadCharger() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val response = api.getChargepointDetail(getReferenceData(), chargerSparse.id)
|
||||
charger = response.data!!
|
||||
|
||||
val photo = charger?.photos?.firstOrNull()
|
||||
photo?.let {
|
||||
val size = (carContext.resources.displayMetrics.density * 64).roundToInt()
|
||||
val url = photo.getUrl(size = size)
|
||||
val request = ImageRequest.Builder(carContext).data(url).build()
|
||||
this@ChargerDetailScreen.photo =
|
||||
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
|
||||
}
|
||||
|
||||
availability = charger?.let { getAvailability(it).data }
|
||||
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
|
||||
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
|
||||
val available = status.count { it == ChargepointStatus.AVAILABLE }
|
||||
val allFaulted = status.all { it == ChargepointStatus.FAULTED }
|
||||
|
||||
return if (unknown) {
|
||||
CarColor.DEFAULT
|
||||
} else if (available > 0) {
|
||||
CarColor.GREEN
|
||||
} else if (allFaulted) {
|
||||
CarColor.RED
|
||||
} else {
|
||||
CarColor.BLUE
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.net.Uri
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
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.nameForPlugType
|
||||
import net.vonforst.evmap.api.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
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
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class ChargerDetailScreen(ctx: CarContext, val chargerSparse: ChargeLocation) : Screen(ctx) {
|
||||
var charger: ChargeLocation? = null
|
||||
var photo: Bitmap? = null
|
||||
private var availability: ChargeLocationStatus? = null
|
||||
|
||||
val prefs = PreferenceDataSource(ctx)
|
||||
private val db = AppDatabase.getInstance(carContext)
|
||||
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()
|
||||
|
||||
return PaneTemplate.Builder(
|
||||
Pane.Builder().apply {
|
||||
charger?.let { charger ->
|
||||
addRow(Row.Builder().apply {
|
||||
setTitle(charger.address.toString())
|
||||
|
||||
val icon = iconGen.getBitmap(
|
||||
tint = getMarkerTint(charger),
|
||||
fault = charger.faultReport != null,
|
||||
multi = charger.isMulti()
|
||||
)
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(icon)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
|
||||
val chargepointsText = SpannableStringBuilder()
|
||||
charger.chargepointsMerged.forEachIndexed { i, cp ->
|
||||
if (i > 0) chargepointsText.append(" · ")
|
||||
chargepointsText.append(
|
||||
"${cp.count}× ${
|
||||
nameForPlugType(
|
||||
carContext.stringProvider(),
|
||||
cp.type
|
||||
)
|
||||
} ${cp.formatPower()}"
|
||||
)
|
||||
availability?.status?.get(cp)?.let { status ->
|
||||
chargepointsText.append(
|
||||
" (${availabilityText(status)}/${cp.count})",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
}
|
||||
addText(chargepointsText)
|
||||
}.build())
|
||||
addRow(Row.Builder().apply {
|
||||
photo?.let {
|
||||
setImage(
|
||||
CarIcon.Builder(IconCompat.createWithBitmap(photo)).build(),
|
||||
Row.IMAGE_TYPE_LARGE
|
||||
)
|
||||
}
|
||||
val operatorText = StringBuilder().apply {
|
||||
charger.operator?.let { append(it) }
|
||||
charger.network?.let {
|
||||
if (isNotEmpty()) append(" · ")
|
||||
append(it)
|
||||
}
|
||||
}.ifEmpty {
|
||||
carContext.getString(R.string.unknown_operator)
|
||||
}
|
||||
setTitle(operatorText)
|
||||
|
||||
charger.cost?.let { addText(it.getStatusText(carContext, emoji = true)) }
|
||||
charger.faultReport?.created?.let {
|
||||
addText(
|
||||
carContext.getString(
|
||||
R.string.auto_fault_report_date,
|
||||
it.atZone(ZoneId.systemDefault())
|
||||
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT))
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/*val types = charger.chargepoints.map { it.type }.distinct()
|
||||
if (types.size == 1) {
|
||||
setImage(
|
||||
CarIcon.of(IconCompat.createWithResource(carContext, iconForPlugType(types[0]))),
|
||||
Row.IMAGE_TYPE_ICON)
|
||||
}*/
|
||||
}.build())
|
||||
addAction(Action.Builder()
|
||||
.setIcon(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_navigation
|
||||
)
|
||||
).build()
|
||||
)
|
||||
.setTitle(carContext.getString(R.string.navigate))
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener {
|
||||
navigateToCharger(charger)
|
||||
}
|
||||
.build())
|
||||
addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.open_in_app))
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
val intent = Intent(carContext, MapsActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(EXTRA_CHARGER_ID, charger.id)
|
||||
.putExtra(EXTRA_LAT, charger.coordinates.lat)
|
||||
.putExtra(EXTRA_LON, charger.coordinates.lng)
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
})
|
||||
.build()
|
||||
)
|
||||
} ?: setLoading(true)
|
||||
}.build()
|
||||
).apply {
|
||||
setTitle(chargerSparse.name)
|
||||
setHeaderAction(Action.BACK)
|
||||
}.build()
|
||||
}
|
||||
|
||||
private fun navigateToCharger(charger: ChargeLocation) {
|
||||
val coord = charger.coordinates
|
||||
val intent =
|
||||
Intent(
|
||||
CarContext.ACTION_NAVIGATE,
|
||||
Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
|
||||
)
|
||||
carContext.startCarApp(intent)
|
||||
}
|
||||
|
||||
private fun loadCharger() {
|
||||
val referenceData = referenceData.value ?: return
|
||||
lifecycleScope.launch {
|
||||
val response = api.getChargepointDetail(referenceData, chargerSparse.id)
|
||||
if (response.status == Status.SUCCESS) {
|
||||
charger = response.data!!
|
||||
|
||||
val photo = charger?.photos?.firstOrNull()
|
||||
photo?.let {
|
||||
val size = (carContext.resources.displayMetrics.density * 64).roundToInt()
|
||||
val url = photo.getUrl(size = size)
|
||||
val request = ImageRequest.Builder(carContext).data(url).build()
|
||||
this@ChargerDetailScreen.photo =
|
||||
(carContext.imageLoader.execute(request).drawable as BitmapDrawable).bitmap
|
||||
}
|
||||
|
||||
availability = charger?.let { getAvailability(it).data }
|
||||
|
||||
invalidate()
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
90
app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt
Normal file
90
app/src/google/java/net/vonforst/evmap/auto/FilterScreen.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
313
app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt
Normal file
313
app/src/google/java/net/vonforst/evmap/auto/MapScreen.kt
Normal file
@@ -0,0 +1,313 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.location.Location
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.car.app.CarContext
|
||||
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.*
|
||||
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.stringProvider
|
||||
import net.vonforst.evmap.model.ChargeLocation
|
||||
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.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
|
||||
|
||||
/**
|
||||
* Main map screen showing either nearby chargers or favorites
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class MapScreen(ctx: CarContext, val session: EVMapSession, val favorites: Boolean = false) :
|
||||
Screen(ctx), LocationAwareScreen {
|
||||
private var updateCoroutine: Job? = null
|
||||
private var numUpdates = 0
|
||||
private val maxNumUpdates = 3
|
||||
|
||||
private var location: Location? = null
|
||||
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)
|
||||
}
|
||||
private val searchRadius = 5 // kilometers
|
||||
private val updateThreshold = 2000 // meters
|
||||
private val availabilityUpdateThreshold = Duration.ofMinutes(1)
|
||||
private var availabilities: MutableMap<Long, Pair<ZonedDateTime, ChargeLocationStatus>> =
|
||||
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 {
|
||||
setTitle(
|
||||
carContext.getString(
|
||||
if (favorites) {
|
||||
R.string.auto_favorites
|
||||
} else {
|
||||
R.string.auto_chargers_closeby
|
||||
}
|
||||
)
|
||||
)
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
} ?: setLoading(true)
|
||||
chargers?.take(maxRows)?.let { chargerList ->
|
||||
val builder = ItemList.Builder()
|
||||
// only show the city if not all chargers are in the same city
|
||||
val showCity = chargerList.map { it.address.city }.distinct().size > 1
|
||||
chargerList.forEach { charger ->
|
||||
builder.addItem(formatCharger(charger, showCity))
|
||||
}
|
||||
builder.setNoItemsMessage(
|
||||
carContext.getString(
|
||||
if (favorites) {
|
||||
R.string.auto_no_favorites_found
|
||||
} else {
|
||||
R.string.auto_no_chargers_found
|
||||
}
|
||||
)
|
||||
)
|
||||
setItemList(builder.build())
|
||||
} ?: 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()
|
||||
}
|
||||
|
||||
private fun formatCharger(charger: ChargeLocation, showCity: Boolean): Row {
|
||||
val color = ContextCompat.getColor(carContext, getMarkerTint(charger))
|
||||
val place =
|
||||
Place.Builder(CarLocation.create(charger.coordinates.lat, charger.coordinates.lng))
|
||||
.setMarker(
|
||||
PlaceMarker.Builder()
|
||||
.setColor(CarColor.createCustom(color, color))
|
||||
.build()
|
||||
)
|
||||
.build()
|
||||
|
||||
return Row.Builder().apply {
|
||||
// only show the city if not all chargers are in the same city (-> showCity == true)
|
||||
// and the city is not already contained in the charger name
|
||||
if (showCity && charger.address.city != null && charger.address.city !in charger.name) {
|
||||
setTitle(
|
||||
CarText.Builder("${charger.name} · ${charger.address.city}")
|
||||
.addVariant(charger.name)
|
||||
.build())
|
||||
} else {
|
||||
setTitle(charger.name)
|
||||
}
|
||||
|
||||
val text = SpannableStringBuilder()
|
||||
|
||||
// distance
|
||||
location?.let {
|
||||
val distance = distanceBetween(
|
||||
it.latitude, it.longitude,
|
||||
charger.coordinates.lat, charger.coordinates.lng
|
||||
) / 1000
|
||||
text.append(
|
||||
"distance",
|
||||
DistanceSpan.create(Distance.create(distance, Distance.UNIT_KILOMETERS)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
// power
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append("${charger.maxPower.roundToInt()} kW")
|
||||
|
||||
// availability
|
||||
availabilities[charger.id]?.second?.let { av ->
|
||||
val status = av.status.values.flatten()
|
||||
val available = availabilityText(status)
|
||||
val total = charger.chargepoints.sumBy { it.count }
|
||||
|
||||
if (text.isNotEmpty()) text.append(" · ")
|
||||
text.append(
|
||||
"$available/$total",
|
||||
ForegroundCarColorSpan.create(carAvailabilityColor(status)),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
}
|
||||
|
||||
addText(text)
|
||||
setMetadata(
|
||||
Metadata.Builder()
|
||||
.setPlace(place)
|
||||
.build()
|
||||
)
|
||||
|
||||
setOnClickListener {
|
||||
screenManager.push(ChargerDetailScreen(carContext, charger))
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
if (updateCoroutine != null) {
|
||||
// don't update while still loading last update
|
||||
return
|
||||
}
|
||||
|
||||
invalidate()
|
||||
|
||||
if (lastUpdateLocation == null ||
|
||||
location.distanceTo(lastUpdateLocation) > updateThreshold
|
||||
) {
|
||||
lastUpdateLocation = location
|
||||
// update displayed chargers
|
||||
loadChargers()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChargers() {
|
||||
val location = location ?: return
|
||||
val referenceData = referenceData.value ?: return
|
||||
val filters = filtersWithValue.value ?: return
|
||||
|
||||
numUpdates++
|
||||
println(numUpdates)
|
||||
if (numUpdates > maxNumUpdates) {
|
||||
CarToast.makeText(carContext, R.string.auto_no_refresh_possible, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
updateCoroutine = lifecycleScope.launch {
|
||||
try {
|
||||
// load chargers
|
||||
if (favorites) {
|
||||
chargers = db.chargeLocationsDao().getAllChargeLocationsAsync().sortedBy {
|
||||
distanceBetween(
|
||||
location.latitude, location.longitude,
|
||||
it.coordinates.lat, it.coordinates.lng
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius,
|
||||
zoom = 16f,
|
||||
filters
|
||||
)
|
||||
chargers = response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
chargers?.let {
|
||||
if (it.size < 6) {
|
||||
// try again with larger radius
|
||||
val response = api.getChargepointsRadius(
|
||||
referenceData,
|
||||
LatLng.fromLocation(location),
|
||||
searchRadius * 10,
|
||||
zoom = 16f,
|
||||
filters
|
||||
)
|
||||
chargers =
|
||||
response.data?.filterIsInstance(ChargeLocation::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove outdated availabilities
|
||||
availabilities = availabilities.filter {
|
||||
Duration.between(
|
||||
it.value.first,
|
||||
ZonedDateTime.now()
|
||||
) > availabilityUpdateThreshold
|
||||
}.toMutableMap()
|
||||
|
||||
// update availabilities
|
||||
chargers?.take(maxRows)?.map {
|
||||
lifecycleScope.async {
|
||||
// update only if not yet stored
|
||||
if (!availabilities.containsKey(it.id)) {
|
||||
val date = ZonedDateTime.now()
|
||||
val availability = getAvailability(it).data
|
||||
if (availability != null) {
|
||||
availabilities[it.id] = date to availability
|
||||
}
|
||||
}
|
||||
}
|
||||
}?.awaitAll()
|
||||
|
||||
updateCoroutine = null
|
||||
invalidate()
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
CarToast.makeText(carContext, R.string.connection_error, CarToast.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.CarToast
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.*
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
/**
|
||||
* Screen to grant location permission
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class PermissionScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx) {
|
||||
override fun onGetTemplate(): Template {
|
||||
return MessageTemplate.Builder(carContext.getString(R.string.auto_location_permission_needed))
|
||||
.setTitle(carContext.getString(R.string.app_name))
|
||||
.setHeaderAction(Action.APP_ICON)
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.grant_on_phone))
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.setOnClickListener(ParkedOnlyOnClickListener.create {
|
||||
val intent = Intent(carContext, PermissionActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.putExtra(
|
||||
PermissionActivity.EXTRA_RESULT_RECEIVER,
|
||||
object : ResultReceiver(null) {
|
||||
override fun onReceiveResult(
|
||||
resultCode: Int,
|
||||
resultData: Bundle?
|
||||
) {
|
||||
if (resultData!!.getBoolean(PermissionActivity.RESULT_GRANTED)) {
|
||||
session.bindLocationService()
|
||||
screenManager.push(
|
||||
WelcomeScreen(
|
||||
carContext,
|
||||
session
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
carContext.startActivity(intent)
|
||||
CarToast.makeText(
|
||||
carContext,
|
||||
R.string.opened_on_phone,
|
||||
CarToast.LENGTH_LONG
|
||||
).show()
|
||||
})
|
||||
.build()
|
||||
)
|
||||
.addAction(
|
||||
Action.Builder()
|
||||
.setTitle(carContext.getString(R.string.cancel))
|
||||
.setOnClickListener {
|
||||
carContext.finishCarApp()
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
20
app/src/google/java/net/vonforst/evmap/auto/Utils.kt
Normal file
20
app/src/google/java/net/vonforst/evmap/auto/Utils.kt
Normal file
@@ -0,0 +1,20 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import androidx.car.app.model.CarColor
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
|
||||
fun carAvailabilityColor(status: List<ChargepointStatus>): CarColor {
|
||||
val unknown = status.any { it == ChargepointStatus.UNKNOWN }
|
||||
val available = status.count { it == ChargepointStatus.AVAILABLE }
|
||||
val allFaulted = status.all { it == ChargepointStatus.FAULTED }
|
||||
|
||||
return if (unknown) {
|
||||
CarColor.DEFAULT
|
||||
} else if (available > 0) {
|
||||
CarColor.GREEN
|
||||
} else if (allFaulted) {
|
||||
CarColor.RED
|
||||
} else {
|
||||
CarColor.BLUE
|
||||
}
|
||||
}
|
||||
70
app/src/google/java/net/vonforst/evmap/auto/WelcomeScreen.kt
Normal file
70
app/src/google/java/net/vonforst/evmap/auto/WelcomeScreen.kt
Normal file
@@ -0,0 +1,70 @@
|
||||
package net.vonforst.evmap.auto
|
||||
|
||||
import android.location.Location
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.*
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
/**
|
||||
* Welcome screen with selection between favorites and nearby chargers
|
||||
*/
|
||||
@androidx.car.app.annotations.ExperimentalCarApi
|
||||
class WelcomeScreen(ctx: CarContext, val session: EVMapSession) : Screen(ctx), LocationAwareScreen {
|
||||
private var location: Location? = null
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
session.mapScreen = this
|
||||
return PlaceListMapTemplate.Builder().apply {
|
||||
setTitle(carContext.getString(R.string.app_name))
|
||||
location?.let {
|
||||
setAnchor(Place.Builder(CarLocation.create(it)).build())
|
||||
}
|
||||
setItemList(ItemList.Builder().apply {
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_chargers_closeby))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_address
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(MapScreen(carContext, session, favorites = false))
|
||||
}
|
||||
.build())
|
||||
addItem(
|
||||
Row.Builder()
|
||||
.setTitle(carContext.getString(R.string.auto_favorites))
|
||||
.setImage(
|
||||
CarIcon.Builder(
|
||||
IconCompat.createWithResource(
|
||||
carContext,
|
||||
R.drawable.ic_fav
|
||||
)
|
||||
)
|
||||
.setTint(CarColor.DEFAULT).build()
|
||||
)
|
||||
.setBrowsable(true)
|
||||
.setOnClickListener {
|
||||
screenManager.push(MapScreen(carContext, session, favorites = true))
|
||||
}
|
||||
.build())
|
||||
}.build())
|
||||
setCurrentLocationEnabled(true)
|
||||
setHeaderAction(Action.APP_ICON)
|
||||
build()
|
||||
}.build()
|
||||
}
|
||||
|
||||
override fun updateLocation(location: Location) {
|
||||
this.location = location
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -168,7 +168,7 @@ class CheckableConnectorAdapter : DataBindingAdapter<Chargepoint>() {
|
||||
}
|
||||
root.setOnCheckedChangeListener { v: View, checked: Boolean ->
|
||||
if (checked) {
|
||||
checkedItem = position
|
||||
checkedItem = holder.bindingAdapterPosition
|
||||
root.post {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import com.squareup.moshi.Moshi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.supervisorScope
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.BuildConfig
|
||||
import net.vonforst.evmap.R
|
||||
@@ -362,30 +363,32 @@ class GoingElectricApiWrapper(
|
||||
|
||||
override suspend fun getReferenceData(): Resource<GEReferenceData> =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val plugs = async { api.getPlugs() }
|
||||
val chargeCards = async { api.getChargeCards() }
|
||||
val networks = async { api.getNetworks() }
|
||||
supervisorScope {
|
||||
try {
|
||||
val plugs = async { api.getPlugs() }
|
||||
val chargeCards = async { api.getChargeCards() }
|
||||
val networks = async { api.getNetworks() }
|
||||
|
||||
val plugsResponse = plugs.await()
|
||||
val chargeCardsResponse = chargeCards.await()
|
||||
val networksResponse = networks.await()
|
||||
val plugsResponse = plugs.await()
|
||||
val chargeCardsResponse = chargeCards.await()
|
||||
val networksResponse = networks.await()
|
||||
|
||||
val responses = listOf(plugsResponse, chargeCardsResponse, networksResponse)
|
||||
val responses = listOf(plugsResponse, chargeCardsResponse, networksResponse)
|
||||
|
||||
if (responses.map { it.isSuccessful }.all { it }) {
|
||||
Resource.success(
|
||||
GEReferenceData(
|
||||
plugsResponse.body()!!.result,
|
||||
networksResponse.body()!!.result,
|
||||
chargeCardsResponse.body()!!.result
|
||||
if (responses.map { it.isSuccessful }.all { it }) {
|
||||
Resource.success(
|
||||
GEReferenceData(
|
||||
plugsResponse.body()!!.result,
|
||||
networksResponse.body()!!.result,
|
||||
chargeCardsResponse.body()!!.result
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Resource.error(responses.find { !it.isSuccessful }!!.message(), null)
|
||||
} else {
|
||||
Resource.error(responses.find { !it.isSuccessful }!!.message(), null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Resource.error(e.message, null)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Resource.error(e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ data class GEChargeLocation(
|
||||
) : GEChargepointListItem() {
|
||||
override fun convert(apikey: String) = ChargeLocation(
|
||||
id,
|
||||
"goingelectric",
|
||||
name,
|
||||
coordinates.convert(),
|
||||
address.convert(),
|
||||
|
||||
@@ -46,6 +46,7 @@ data class OCMChargepoint(
|
||||
) {
|
||||
fun convert(refData: OCMReferenceData) = ChargeLocation(
|
||||
id,
|
||||
"openchargemap",
|
||||
addressInfo.title,
|
||||
Coordinate(addressInfo.latitude, addressInfo.longitude),
|
||||
addressInfo.toAddress(refData),
|
||||
@@ -77,7 +78,7 @@ data class OCMChargepoint(
|
||||
val comment = userComments.filter { it.commentTypeId == faultReportCommentType }
|
||||
.maxByOrNull { it.dateCreated }
|
||||
if (comment != null) {
|
||||
return FaultReport(comment.dateCreated.toInstant(), comment.comment)
|
||||
return FaultReport(comment.dateCreated.toInstant(), comment.comment ?: "")
|
||||
}
|
||||
}
|
||||
if (statusType != null && statusType.id in faultStatuses) {
|
||||
@@ -229,7 +230,7 @@ data class OCMMediaItem(
|
||||
data class OCMUserComment(
|
||||
@Json(name = "ID") val id: Long,
|
||||
@Json(name = "CommentTypeID") val commentTypeId: Long,
|
||||
@Json(name = "Comment") val comment: String,
|
||||
@Json(name = "Comment") val comment: String?,
|
||||
@Json(name = "UserName") val userName: String,
|
||||
@Json(name = "DateCreated") val dateCreated: ZonedDateTime
|
||||
)
|
||||
@@ -257,4 +258,4 @@ class OCMChargerPhotoAdapter(
|
||||
else -> largeUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,15 +36,8 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
prefs = PreferenceDataSource(requireContext())
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
myVehiclePreference = findPreference("chargeprice_my_vehicle")!!
|
||||
myVehiclePreference.isEnabled = false
|
||||
vm.vehicles.observe(viewLifecycleOwner) { res ->
|
||||
@@ -130,6 +123,13 @@ class SettingsFragment : PreferenceFragmentCompat(),
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
|
||||
|
||||
val navController = findNavController()
|
||||
val toolbar = requireView().findViewById(R.id.toolbar) as Toolbar
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,14 @@ class ChargepriceViewModel(application: Application, chargepriceApiKey: String)
|
||||
MutableLiveData<List<Float>>().apply {
|
||||
value = prefs.chargepriceBatteryRange
|
||||
observeForever {
|
||||
prefs.chargepriceBatteryRange = it
|
||||
if (it[0] == it[1]) {
|
||||
value = if (it[0] < 1.0) {
|
||||
listOf(it[0], it[1] + 1)
|
||||
} else {
|
||||
listOf(it[0] - 1, it[1])
|
||||
}
|
||||
}
|
||||
prefs.chargepriceBatteryRange = value!!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
81
app/src/main/java/net/vonforst/evmap/viewmodel/Common.kt
Normal file
81
app/src/main/java/net/vonforst/evmap/viewmodel/Common.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 net.vonforst.evmap.api.ChargepointApi
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
@@ -18,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
|
||||
|
||||
@@ -53,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)
|
||||
@@ -270,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
|
||||
}
|
||||
@@ -378,9 +342,12 @@ class MapViewModel(application: Application) : AndroidViewModel(application) {
|
||||
availability.value = getAvailability(charger)
|
||||
}
|
||||
|
||||
private var chargerLoadingTask: Job? = null
|
||||
|
||||
private fun loadChargerDetails(charger: ChargeLocation, referenceData: ReferenceData) {
|
||||
chargerDetails.value = Resource.loading(null)
|
||||
viewModelScope.launch {
|
||||
chargerLoadingTask?.cancel()
|
||||
chargerLoadingTask = viewModelScope.launch {
|
||||
try {
|
||||
chargerDetails.value = api.getChargepointDetail(referenceData, charger.id)
|
||||
} catch (e: IOException) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
2
fastlane/metadata/android/de-DE/changelogs/51.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/51.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Android Auto: Anzeige des Ortsnamens (wenn genug Platz und nicht eindeutig)
|
||||
- Abstürze behoben
|
||||
2
fastlane/metadata/android/de-DE/changelogs/52.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/52.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Chargeprice: Auswahl von gleichem Start- und Endladestand verhindert
|
||||
- Abstürze behoben
|
||||
@@ -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.
|
||||
2
fastlane/metadata/android/en-US/changelogs/51.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/51.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Android Auto: Show place name next to charger name (if enough room available)
|
||||
- Fixed various crashes
|
||||
2
fastlane/metadata/android/en-US/changelogs/52.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/52.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
- Chargeprice: Prevent selection of same state of charge for start and end
|
||||
- Fixed crashes
|
||||
@@ -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.
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user