mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 16:17:45 -05:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd902f86a4 | ||
|
|
c3b583772b | ||
|
|
b19dab7e47 | ||
|
|
4bea049a7b | ||
|
|
5c4dd958f9 | ||
|
|
dba9bf6d10 | ||
|
|
873a54c3ca | ||
|
|
ed1647bb55 | ||
|
|
cfb6af28c0 | ||
|
|
6be926c308 | ||
|
|
b1c2844360 | ||
|
|
22ff42f3cf | ||
|
|
918a6eee58 | ||
|
|
2dcf03f831 | ||
|
|
f2e7cfbb36 | ||
|
|
d4d394dbd3 | ||
|
|
1f71b435c4 | ||
|
|
ec19a55db8 | ||
|
|
84bbdaf4ec | ||
|
|
febc72f190 | ||
|
|
20a1dea2cd | ||
|
|
6e93e602b1 | ||
|
|
083643fa41 |
19
_img/connectors/connector_typ1.svg
Normal file
19
_img/connectors/connector_typ1.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg id="Ebene_5" data-name="Ebene 5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1,.cls-2,.cls-3{fill:none;}.cls-2,.cls-3{stroke:#000;stroke-miterlimit:10;}.cls-2{stroke-width:2px;}.cls-3{stroke-width:0.5px;}
|
||||
</style>
|
||||
</defs>
|
||||
<title>connector_typ1</title>
|
||||
<path class="cls-1" d="M12,12H36V36H12Z" />
|
||||
<circle cx="15.79" cy="8.26" r="1.89" />
|
||||
<circle cx="16.74" cy="14" r="1.18" />
|
||||
<circle cx="7.26" cy="14" r="1.18" />
|
||||
<circle cx="8.21" cy="8.26" r="1.89" />
|
||||
<circle cx="12" cy="17.74" r="1.89" />
|
||||
<circle class="cls-2" cx="12" cy="12.05" r="9" />
|
||||
<rect x="10.58" y="21.05" width="2.84" height="1.89" />
|
||||
<line class="cls-3" x1="10.5" y1="1" x2="13.5" y2="1" />
|
||||
<polygon points="13.5 0.4 13.5 2.5 15.5 3.5 14.5 0.5 13.5 0.4" />
|
||||
<polygon points="10.5 0.4 10.5 2.5 8.5 3.5 9.5 0.5 10.5 0.4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 913 B |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode 2
|
||||
versionName "0.0.2"
|
||||
versionCode 3
|
||||
versionName "0.0.3"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -62,13 +62,14 @@ dependencies {
|
||||
implementation "androidx.activity:activity-ktx:1.1.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.2.4"
|
||||
implementation 'androidx.cardview:cardview:1.0.0'
|
||||
implementation 'androidx.core:core:1.3.0-rc01'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.0'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.maps.android:android-maps-utils:0.5'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:c2dcf0dc'
|
||||
implementation 'com.github.johan12345:CustomBottomSheetBehavior:73dd449f6f'
|
||||
implementation 'com.google.android.gms:play-services-maps:17.0.0'
|
||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
implementation 'com.google.android.libraries.places:places:2.2.0'
|
||||
@@ -90,6 +91,12 @@ dependencies {
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
|
||||
// room library
|
||||
def room_version = "2.2.5"
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
kapt "androidx.room:room-compiler:$room_version"
|
||||
implementation "androidx.room:room-ktx:$room_version"
|
||||
|
||||
// debug tools
|
||||
implementation 'com.facebook.stetho:stetho:1.5.1'
|
||||
implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1'
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.navigation.ui.setupWithNavController
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
|
||||
const val REQUEST_LOCATION_PERMISSION = 1
|
||||
|
||||
@@ -26,6 +27,7 @@ class MapsActivity : AppCompatActivity() {
|
||||
private lateinit var navController: NavController
|
||||
lateinit var appBarConfiguration: AppBarConfiguration
|
||||
var fragmentCallback: FragmentCallback? = null
|
||||
private lateinit var prefs: PreferenceDataSource
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -42,6 +44,8 @@ class MapsActivity : AppCompatActivity() {
|
||||
findViewById<DrawerLayout>(R.id.drawer_layout)
|
||||
)
|
||||
findViewById<NavigationView>(R.id.nav_view).setupWithNavController(navController)
|
||||
|
||||
prefs = PreferenceDataSource(this)
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
@@ -56,11 +60,11 @@ class MapsActivity : AppCompatActivity() {
|
||||
// google maps navigation
|
||||
intent.data = Uri.parse("google.navigation:q=${coord.lat},${coord.lng}")
|
||||
val pm = packageManager
|
||||
if (intent.resolveActivity(pm) != null) {
|
||||
if (intent.resolveActivity(pm) != null && prefs.navigateUseMaps) {
|
||||
startActivity(intent);
|
||||
} else {
|
||||
// fallback: generic geo intent
|
||||
intent.data = Uri.parse("geo:${coord.lat},${coord.lng}")
|
||||
intent.data = Uri.parse("geo:0,0?q=${coord.lat},${coord.lng}(${charger.name})")
|
||||
if (intent.resolveActivity(pm) != null) {
|
||||
startActivity(intent);
|
||||
} else {
|
||||
@@ -78,4 +82,12 @@ class MapsActivity : AppCompatActivity() {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun shareUrl(url: String) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
setType("text/plain")
|
||||
putExtra(Intent.EXTRA_TEXT, url)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.api.availability.ChargepointStatus
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
|
||||
interface Equatable {
|
||||
override fun equals(other: Any?): Boolean;
|
||||
@@ -29,15 +30,15 @@ abstract class DataBindingAdapter<T : Equatable>() :
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder<T>, position: Int) =
|
||||
holder.bind(getItem(position))
|
||||
bind(holder, getItem(position))
|
||||
|
||||
class ViewHolder<T>(private val binding: ViewDataBinding) :
|
||||
class ViewHolder<T>(val binding: ViewDataBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
}
|
||||
|
||||
fun bind(item: T) {
|
||||
binding.setVariable(BR.item, item)
|
||||
binding.executePendingBindings()
|
||||
}
|
||||
open fun bind(holder: ViewHolder<T>, item: T) {
|
||||
holder.binding.setVariable(BR.item, item)
|
||||
holder.binding.executePendingBindings()
|
||||
}
|
||||
|
||||
class DiffCallback<T : Equatable> : DiffUtil.ItemCallback<T>() {
|
||||
@@ -70,7 +71,8 @@ class DetailAdapter : DataBindingAdapter<DetailAdapter.Detail>() {
|
||||
val icon: Int,
|
||||
val contentDescription: Int,
|
||||
val text: CharSequence,
|
||||
val detailText: CharSequence? = null
|
||||
val detailText: CharSequence? = null,
|
||||
val links: Boolean = true
|
||||
) : Equatable
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_detail
|
||||
@@ -109,6 +111,25 @@ fun buildDetails(loc: ChargeLocation?, ctx: Context): List<DetailAdapter.Detail>
|
||||
loc.cost.getStatusText(ctx),
|
||||
loc.cost.descriptionLong ?: loc.cost.descriptionShort
|
||||
)
|
||||
else null
|
||||
else null,
|
||||
DetailAdapter.Detail(
|
||||
R.drawable.ic_location,
|
||||
R.string.coordinates,
|
||||
loc.coordinates.formatDMS(),
|
||||
loc.coordinates.formatDecimal(),
|
||||
false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
class FavoritesAdapter(val vm: FavoritesViewModel) :
|
||||
DataBindingAdapter<FavoritesViewModel.FavoritesListItem>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int = R.layout.item_favorite
|
||||
|
||||
override fun getItemId(position: Int): Long = getItem(position).charger.id
|
||||
}
|
||||
@@ -38,13 +38,17 @@ suspend fun Call.await(): Response {
|
||||
|
||||
const val earthRadiusKm: Double = 6372.8
|
||||
|
||||
/**
|
||||
* Calculates the distance between two points on Earth in meters.
|
||||
* Latitude and longitude should be given in degrees.
|
||||
*/
|
||||
fun distanceBetween(
|
||||
startLatitude: Double, startLongitude: Double,
|
||||
endLatitude: Double, endLongitude: Double
|
||||
): Double {
|
||||
// see https://rosettacode.org/wiki/Haversine_formula#Java
|
||||
val dLat = Math.toRadians(endLatitude - startLatitude);
|
||||
val dLon = Math.toRadians(endLongitude - endLongitude);
|
||||
val dLon = Math.toRadians(endLongitude - startLongitude);
|
||||
val originLat = Math.toRadians(startLatitude);
|
||||
val destinationLat = Math.toRadians(endLatitude);
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import com.facebook.stetho.okhttp3.StethoInterceptor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.api.await
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import retrofit2.HttpException
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@@ -14,6 +18,8 @@ interface AvailabilityDetector {
|
||||
}
|
||||
|
||||
abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : AvailabilityDetector {
|
||||
protected val radius = 150 // max radius in meters
|
||||
|
||||
protected suspend fun httpGet(url: String): String {
|
||||
val request = Request.Builder().url(url).build()
|
||||
val response = client.newCall(request).await()
|
||||
@@ -46,35 +52,55 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
return filter.getOrNull(0)
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal fun matchChargepoints(
|
||||
connectors: Map<Long, Pair<Double, String>>,
|
||||
chargepoints: List<Chargepoint>
|
||||
): Map<Chargepoint, Set<Long>> {
|
||||
// iterate over each connector type
|
||||
val types = connectors.map { it.value.second }.distinct().toSet()
|
||||
val geTypes = chargepoints.map { it.type }.distinct().toSet()
|
||||
if (types != geTypes) throw AvailabilityDetectorException("chargepoints do not match")
|
||||
return types.flatMap { type ->
|
||||
// find connectors of this type
|
||||
val connsOfType = connectors.filter { it.value.second == type }
|
||||
// find powers this connector is available as
|
||||
val powers = connsOfType.map { it.value.first }.distinct().sorted()
|
||||
// find corresponding powers in GE data
|
||||
val gePowers =
|
||||
chargepoints.filter { it.type == type }.map { it.power }.distinct().sorted()
|
||||
|
||||
protected fun matchChargepoints(
|
||||
connectors: Map<Long, Pair<Double, String>>,
|
||||
chargepoints: List<Chargepoint>
|
||||
): Map<Chargepoint, Set<Long>> {
|
||||
// iterate over each connector type
|
||||
val types = connectors.map { it.value.second }.distinct().toSet()
|
||||
val geTypes = chargepoints.map { it.type }.distinct().toSet()
|
||||
if (types != geTypes) throw AvailabilityDetectorException("chargepoints do not match")
|
||||
return types.flatMap { type ->
|
||||
// find connectors of this type
|
||||
val connsOfType = connectors.filter { it.value.second == type }
|
||||
// find powers this connector is available as
|
||||
val powers = connsOfType.map { it.value.first }.distinct().sorted()
|
||||
// find corresponding powers in GE data
|
||||
val gePowers =
|
||||
chargepoints.filter { it.type == type }.map { it.power }.distinct().sorted()
|
||||
|
||||
// if the distinct number of powers is the same, try to match.
|
||||
if (powers.size == gePowers.size) {
|
||||
gePowers.zip(powers).map { (gePower, power) ->
|
||||
val chargepoint = chargepoints.find { it.type == type && it.power == gePower }!!
|
||||
val ids = connsOfType.filter { it.value.first == power }.keys
|
||||
chargepoint to ids
|
||||
// if the distinct number of powers is the same, try to match.
|
||||
if (powers.size == gePowers.size) {
|
||||
gePowers.zip(powers).map { (gePower, power) ->
|
||||
val chargepoint =
|
||||
chargepoints.find { it.type == type && it.power == gePower }!!
|
||||
val ids = connsOfType.filter { it.value.first == power }.keys
|
||||
if (chargepoint.count != ids.size) {
|
||||
throw AvailabilityDetectorException("chargepoints do not match")
|
||||
}
|
||||
chargepoint to ids
|
||||
}
|
||||
} else if (powers.size == 1 && gePowers.size == 2
|
||||
&& chargepoints.sumBy { it.count } == connsOfType.size
|
||||
) {
|
||||
// special case: dual charger(s) with load balancing
|
||||
// GoingElectric shows 2 different powers, NewMotion just one
|
||||
val allIds = connsOfType.keys.toList()
|
||||
var i = 0
|
||||
gePowers.map { gePower ->
|
||||
val chargepoint =
|
||||
chargepoints.find { it.type == type && it.power == gePower }!!
|
||||
val ids = allIds.subList(i, i + chargepoint.count).toSet()
|
||||
i += chargepoint.count
|
||||
chargepoint to ids
|
||||
}
|
||||
// TODO: this will not necessarily first fill up the higher-power chargepoint
|
||||
} else {
|
||||
throw AvailabilityDetectorException("chargepoints do not match")
|
||||
}
|
||||
} else {
|
||||
throw AvailabilityDetectorException("chargepoints do not match")
|
||||
}
|
||||
}.toMap()
|
||||
}.toMap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,4 +130,26 @@ val availabilityDetectors = listOf(
|
||||
okhttp,
|
||||
"6336fe713f2eb7fa04b97ff6651b76f8"
|
||||
) // SW Kiel*/
|
||||
)
|
||||
)
|
||||
|
||||
suspend fun getAvailability(charger: ChargeLocation): Resource<ChargeLocationStatus> {
|
||||
var value: Resource<ChargeLocationStatus>? = null
|
||||
withContext(Dispatchers.IO) {
|
||||
for (ad in availabilityDetectors) {
|
||||
try {
|
||||
value = Resource.success(ad.getAvailability(charger))
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: HttpException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: AvailabilityDetectorException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
return value ?: Resource.error(null, null)
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import okhttp3.OkHttpClient
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
|
||||
private const val radius = 200 // max radius in meters
|
||||
|
||||
class ChargecloudAvailabilityDetector(
|
||||
client: OkHttpClient,
|
||||
private val operatorId: String
|
||||
|
||||
@@ -99,6 +99,16 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
distanceBetween(marker.coordinates.latitude, marker.coordinates.longitude, lat, lng)
|
||||
} ?: throw AvailabilityDetectorException("no candidates found.")
|
||||
|
||||
if (distanceBetween(
|
||||
nearest.coordinates.latitude,
|
||||
nearest.coordinates.longitude,
|
||||
lat,
|
||||
lng
|
||||
) > radius
|
||||
) {
|
||||
throw AvailabilityDetectorException("no candidates found")
|
||||
}
|
||||
|
||||
// combine related stations
|
||||
markers = markers.filter { marker ->
|
||||
distanceBetween(
|
||||
@@ -129,7 +139,9 @@ class NewMotionAvailabilityDetector(client: OkHttpClient, baseUrl: String? = nul
|
||||
val id = connector.uid
|
||||
val power = connector.electricalProperties.getPower()
|
||||
val type = when (connector.connectorType) {
|
||||
"Type3" -> Chargepoint.TYPE_3
|
||||
"Type2" -> Chargepoint.TYPE_2
|
||||
"Type1" -> Chargepoint.TYPE_1
|
||||
"Domestic" -> Chargepoint.SCHUKO
|
||||
"Type2Combo" -> Chargepoint.CCS
|
||||
"TepcoCHAdeMO" -> Chargepoint.CHADEMO
|
||||
|
||||
@@ -3,6 +3,9 @@ package net.vonforst.evmap.api.goingelectric
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.core.text.HtmlCompat
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
@@ -11,6 +14,9 @@ import net.vonforst.evmap.adapter.Equatable
|
||||
import java.time.DayOfWeek
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ChargepointList(
|
||||
@@ -21,11 +27,12 @@ data class ChargepointList(
|
||||
sealed class ChargepointListItem
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
@Entity
|
||||
data class ChargeLocation(
|
||||
@Json(name = "ge_id") val id: Long,
|
||||
@Json(name = "ge_id") @PrimaryKey val id: Long,
|
||||
val name: String,
|
||||
val coordinates: Coordinate,
|
||||
val address: Address,
|
||||
@Embedded val coordinates: Coordinate,
|
||||
@Embedded val address: Address,
|
||||
val chargepoints: List<Chargepoint>,
|
||||
@JsonObjectOrFalse val network: String?,
|
||||
val url: String,
|
||||
@@ -38,9 +45,9 @@ data class ChargeLocation(
|
||||
@JsonObjectOrFalse @Json(name = "location_description") val locationDescription: String?,
|
||||
val photos: List<ChargerPhoto>?,
|
||||
//val chargecards: Boolean?
|
||||
val openinghours: OpeningHours?,
|
||||
val cost: Cost?
|
||||
) : ChargepointListItem() {
|
||||
@Embedded val openinghours: OpeningHours?,
|
||||
@Embedded val cost: Cost?
|
||||
) : ChargepointListItem(), Equatable {
|
||||
val maxPower: Double
|
||||
get() {
|
||||
return chargepoints.map { it.power }.max() ?: 0.0
|
||||
@@ -75,7 +82,7 @@ data class Cost(
|
||||
data class OpeningHours(
|
||||
@Json(name = "24/7") val twentyfourSeven: Boolean,
|
||||
@JsonObjectOrFalse val description: String?,
|
||||
val days: OpeningHoursDays?
|
||||
@Embedded val days: OpeningHoursDays?
|
||||
) {
|
||||
fun getStatusText(ctx: Context): CharSequence {
|
||||
if (twentyfourSeven) {
|
||||
@@ -114,14 +121,14 @@ data class OpeningHours(
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class OpeningHoursDays(
|
||||
val monday: Hours,
|
||||
val tuesday: Hours,
|
||||
val wednesday: Hours,
|
||||
val thursday: Hours,
|
||||
val friday: Hours,
|
||||
val saturday: Hours,
|
||||
val sunday: Hours,
|
||||
val holiday: Hours
|
||||
@Embedded(prefix = "mo") val monday: Hours,
|
||||
@Embedded(prefix = "tu") val tuesday: Hours,
|
||||
@Embedded(prefix = "we") val wednesday: Hours,
|
||||
@Embedded(prefix = "th") val thursday: Hours,
|
||||
@Embedded(prefix = "fr") val friday: Hours,
|
||||
@Embedded(prefix = "sa") val saturday: Hours,
|
||||
@Embedded(prefix = "su") val sunday: Hours,
|
||||
@Embedded(prefix = "ho") val holiday: Hours
|
||||
) {
|
||||
fun getHoursForDate(date: LocalDate): Hours {
|
||||
// TODO: check for holidays
|
||||
@@ -155,7 +162,28 @@ data class ChargeLocationCluster(
|
||||
) : ChargepointListItem()
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Coordinate(val lat: Double, val lng: Double)
|
||||
data class Coordinate(val lat: Double, val lng: Double) {
|
||||
fun formatDMS(): String {
|
||||
return "${dms(lat, false)}, ${dms(lng, true)}"
|
||||
}
|
||||
|
||||
private fun dms(value: Double, lon: Boolean): String {
|
||||
val hemisphere = if (lon) {
|
||||
if (value >= 0) "E" else "W"
|
||||
} else {
|
||||
if (value >= 0) "N" else "S"
|
||||
}
|
||||
val d = abs(value)
|
||||
val degrees = floor(d).toInt()
|
||||
val minutes = floor((d - degrees) * 60).toInt()
|
||||
val seconds = ((d - degrees) * 60 - minutes) * 60
|
||||
return "%d°%02d'%02.1f\"%s".format(Locale.ENGLISH, degrees, minutes, seconds, hemisphere)
|
||||
}
|
||||
|
||||
fun formatDecimal(): String {
|
||||
return "%.6f, %.6f".format(Locale.ENGLISH, lat, lng)
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class Address(
|
||||
@@ -181,7 +209,9 @@ data class Chargepoint(val type: String, val power: Double, val count: Int) : Eq
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TYPE_1 = "Typ1"
|
||||
const val TYPE_2 = "Typ2"
|
||||
const val TYPE_3 = "Typ3"
|
||||
const val CCS = "CCS"
|
||||
const val SCHUKO = "Schuko"
|
||||
const val CHADEMO = "CHAdeMO"
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.gms.location.FusedLocationProviderClient
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.adapter.FavoritesAdapter
|
||||
import net.vonforst.evmap.databinding.FragmentFavoritesBinding
|
||||
import net.vonforst.evmap.viewmodel.FavoritesViewModel
|
||||
import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
class FavoritesFragment : Fragment() {
|
||||
private lateinit var binding: FragmentFavoritesBinding
|
||||
private lateinit var fusedLocationClient: FusedLocationProviderClient
|
||||
|
||||
private val vm: FavoritesViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
FavoritesViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.goingelectric_key)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = DataBindingUtil.inflate(
|
||||
inflater,
|
||||
R.layout.fragment_favorites, container, false
|
||||
)
|
||||
binding.lifecycleOwner = this
|
||||
binding.vm = vm
|
||||
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
|
||||
binding.favsList.apply {
|
||||
adapter = FavoritesAdapter(vm)
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
addItemDecoration(
|
||||
DividerItemDecoration(
|
||||
context, LinearLayoutManager.VERTICAL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
vm.favorites.observe(viewLifecycleOwner, Observer {
|
||||
print(it.toString())
|
||||
})
|
||||
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
fusedLocationClient.lastLocation.addOnSuccessListener { location ->
|
||||
vm.location.value = LatLng(location.latitude, location.longitude)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,15 @@ package net.vonforst.evmap.fragment
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
@@ -33,6 +36,7 @@ import com.google.android.libraries.places.widget.Autocomplete
|
||||
import com.google.android.libraries.places.widget.model.AutocompleteActivityMode
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
|
||||
import kotlinx.android.synthetic.main.fragment_map.*
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
@@ -44,7 +48,10 @@ import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocationCluster
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.databinding.FragmentMapBinding
|
||||
import net.vonforst.evmap.ui.*
|
||||
import net.vonforst.evmap.ui.ChargerIconGenerator
|
||||
import net.vonforst.evmap.ui.ClusterIconGenerator
|
||||
import net.vonforst.evmap.ui.MarkerAnimator
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.viewmodel.GalleryViewModel
|
||||
import net.vonforst.evmap.viewmodel.MapPosition
|
||||
import net.vonforst.evmap.viewmodel.MapViewModel
|
||||
@@ -55,17 +62,25 @@ const val REQUEST_AUTOCOMPLETE = 2
|
||||
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback {
|
||||
private lateinit var binding: FragmentMapBinding
|
||||
private val vm: MapViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory { MapViewModel(getString(R.string.goingelectric_key)) }
|
||||
viewModelFactory {
|
||||
MapViewModel(
|
||||
requireActivity().application,
|
||||
getString(R.string.goingelectric_key)
|
||||
)
|
||||
}
|
||||
})
|
||||
private val galleryVm: GalleryViewModel by activityViewModels()
|
||||
private var map: GoogleMap? = null
|
||||
private lateinit var fusedLocationClient: FusedLocationProviderClient
|
||||
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
|
||||
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
|
||||
private var markers: Map<Marker, ChargeLocation> = emptyMap()
|
||||
private var clusterMarkers: List<Marker> = emptyList()
|
||||
|
||||
private lateinit var clusterIconGenerator: ClusterIconGenerator
|
||||
private lateinit var chargerIconGenerator: ChargerIconGenerator
|
||||
private lateinit var animator: MarkerAnimator
|
||||
private lateinit var favToggle: MenuItem
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -79,10 +94,18 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext())
|
||||
clusterIconGenerator = ClusterIconGenerator(requireContext())
|
||||
chargerIconGenerator = ChargerIconGenerator(requireContext())
|
||||
animator = MarkerAnimator(chargerIconGenerator)
|
||||
|
||||
setHasOptionsMenu(true)
|
||||
postponeEnterTransition()
|
||||
|
||||
binding.root.setOnApplyWindowInsetsListener { v, insets ->
|
||||
binding.detailAppBar.toolbar.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
topMargin = insets.systemWindowInsetTop
|
||||
}
|
||||
insets
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
@@ -90,6 +113,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment
|
||||
mapFragment.getMapAsync(this)
|
||||
bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet)
|
||||
detailAppBarBehavior = MergedAppBarLayoutBehavior.from(binding.detailAppBar)
|
||||
|
||||
binding.detailAppBar.toolbar.inflateMenu(R.menu.detail)
|
||||
favToggle = binding.detailAppBar.toolbar.menu.findItem(R.id.menu_fav)
|
||||
|
||||
setupObservers()
|
||||
setupClickListeners()
|
||||
@@ -145,6 +172,39 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
.build(requireContext())
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
startActivityForResult(intent, REQUEST_AUTOCOMPLETE)
|
||||
|
||||
val imm =
|
||||
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.toggleSoftInput(0, 0)
|
||||
}
|
||||
binding.detailAppBar.toolbar.setNavigationOnClickListener {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
}
|
||||
binding.detailAppBar.toolbar.setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.menu_fav -> {
|
||||
toggleFavorite()
|
||||
true
|
||||
}
|
||||
R.id.menu_share -> {
|
||||
val charger = vm.charger.value?.data
|
||||
if (charger != null) {
|
||||
(activity as? MapsActivity)?.shareUrl("https:${charger.url}")
|
||||
}
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleFavorite() {
|
||||
val favs = vm.favorites.value ?: return
|
||||
val charger = vm.chargerSparse.value ?: return
|
||||
if (favs.find { it.id == charger.id } != null) {
|
||||
vm.deleteFavorite(charger)
|
||||
} else {
|
||||
vm.insertFavorite(charger)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +225,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
}
|
||||
binding.fabDirections.show()
|
||||
detailAppBarBehavior.setToolbarTitle(it.name)
|
||||
updateFavoriteToggle()
|
||||
} else {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
|
||||
}
|
||||
@@ -173,6 +235,19 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val chargepoints = it.data
|
||||
if (chargepoints != null) updateMap(chargepoints)
|
||||
})
|
||||
vm.favorites.observe(viewLifecycleOwner, Observer {
|
||||
updateFavoriteToggle()
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateFavoriteToggle() {
|
||||
val favs = vm.favorites.value ?: return
|
||||
val charger = vm.chargerSparse.value ?: return
|
||||
if (favs.find { it.id == charger.id } != null) {
|
||||
favToggle.setIcon(R.drawable.ic_fav)
|
||||
} else {
|
||||
favToggle.setIcon(R.drawable.ic_fav_no)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupAdapters() {
|
||||
@@ -312,8 +387,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
return ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
private fun updateMap(chargepoints: List<ChargepointListItem>) {
|
||||
@@ -328,7 +402,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
if (!chargepointIds.contains(it.value.id)) {
|
||||
val tint = getMarkerTint(it.value)
|
||||
if (it.key.isVisible) {
|
||||
animateMarkerDisappear(it.key, tint, chargerIconGenerator)
|
||||
animator.animateMarkerDisappear(it.key, tint)
|
||||
} else {
|
||||
it.key.remove()
|
||||
}
|
||||
@@ -348,7 +422,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
chargerIconGenerator.getBitmapDescriptor(tint)
|
||||
)
|
||||
)
|
||||
animateMarkerAppear(marker, tint, chargerIconGenerator)
|
||||
animator.animateMarkerAppear(marker, tint)
|
||||
|
||||
marker to charger
|
||||
}.toMap()
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.vonforst.evmap.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.ui.setupWithNavController
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import net.vonforst.evmap.MapsActivity
|
||||
import net.vonforst.evmap.R
|
||||
|
||||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
|
||||
|
||||
val navController = findNavController()
|
||||
toolbar.setupWithNavController(
|
||||
navController,
|
||||
(requireActivity() as MapsActivity).appBarConfiguration
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference?): Boolean {
|
||||
return when (preference?.key) {
|
||||
else -> super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
|
||||
@Dao
|
||||
interface ChargeLocationsDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(vararg locations: ChargeLocation)
|
||||
|
||||
@Delete
|
||||
suspend fun delete(vararg locations: ChargeLocation)
|
||||
|
||||
@Query("SELECT * FROM chargelocation")
|
||||
fun getAllChargeLocations(): LiveData<List<ChargeLocation>>
|
||||
}
|
||||
26
app/src/main/java/net/vonforst/evmap/storage/Database.kt
Normal file
26
app/src/main/java/net/vonforst/evmap/storage/Database.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
|
||||
@Database(entities = [ChargeLocation::class], version = 1)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun chargeLocationsDao(): ChargeLocationsDao
|
||||
|
||||
companion object {
|
||||
private lateinit var context: Context
|
||||
private val database: AppDatabase by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
|
||||
Room.databaseBuilder(context, AppDatabase::class.java, "evmap.db").build()
|
||||
}
|
||||
|
||||
fun getInstance(context: Context): AppDatabase {
|
||||
this.context = context.applicationContext
|
||||
return database
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
|
||||
class PreferenceDataSource(context: Context) {
|
||||
val sp = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
var navigateUseMaps: Boolean
|
||||
get() = sp.getBoolean("navigate_use_maps", true)
|
||||
set(value) {
|
||||
sp.edit().putBoolean("navigate_use_maps", value).apply()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package net.vonforst.evmap.storage
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import net.vonforst.evmap.api.goingelectric.ChargerPhoto
|
||||
import java.time.LocalTime
|
||||
|
||||
class Converters {
|
||||
val moshi = Moshi.Builder().build()
|
||||
private val chargepointListAdapter by lazy {
|
||||
val type = Types.newParameterizedType(List::class.java, Chargepoint::class.java)
|
||||
moshi.adapter<List<Chargepoint>>(type)
|
||||
}
|
||||
private val chargerPhotoListAdapter by lazy {
|
||||
val type = Types.newParameterizedType(List::class.java, ChargerPhoto::class.java)
|
||||
moshi.adapter<List<ChargerPhoto>>(type)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromChargepointList(value: List<Chargepoint>?): String {
|
||||
return chargepointListAdapter.toJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toChargepointList(value: String): List<Chargepoint>? {
|
||||
return chargepointListAdapter.fromJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromChargerPhotoList(value: List<ChargerPhoto>?): String {
|
||||
return chargerPhotoListAdapter.toJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toChargerPhotoList(value: String): List<ChargerPhoto>? {
|
||||
return chargerPhotoListAdapter.fromJson(value)
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun fromLocalTime(value: LocalTime?): String? {
|
||||
return value?.toString()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
fun toLocalTime(value: String?): LocalTime? {
|
||||
return value.let {
|
||||
LocalTime.parse(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ fun getConnectorItem(view: ImageView, type: String) {
|
||||
Chargepoint.TYPE_2 -> R.drawable.ic_connector_typ2
|
||||
Chargepoint.CEE_BLAU -> R.drawable.ic_connector_cee_blau
|
||||
Chargepoint.CEE_ROT -> R.drawable.ic_connector_cee_rot
|
||||
Chargepoint.TYPE_1 -> R.drawable.ic_connector_typ1
|
||||
// TODO: add other connectors
|
||||
else -> 0
|
||||
}
|
||||
|
||||
@@ -16,47 +16,65 @@ fun getMarkerTint(charger: ChargeLocation): Int = when {
|
||||
else -> R.color.charger_low
|
||||
}
|
||||
|
||||
fun animateMarkerAppear(
|
||||
marker: Marker,
|
||||
tint: Int,
|
||||
gen: ChargerIconGenerator
|
||||
) {
|
||||
ValueAnimator.ofInt(0, 20).apply {
|
||||
duration = 250
|
||||
interpolator = LinearOutSlowInInterpolator()
|
||||
addUpdateListener { animationState ->
|
||||
if (!marker.isVisible) {
|
||||
cancel()
|
||||
return@addUpdateListener
|
||||
}
|
||||
val scale = animationState.animatedValue as Int
|
||||
marker.setIcon(
|
||||
gen.getBitmapDescriptor(tint, scale = scale)
|
||||
)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
class MarkerAnimator(val gen: ChargerIconGenerator) {
|
||||
val animatingMarkers = hashMapOf<Marker, ValueAnimator>()
|
||||
|
||||
fun animateMarkerDisappear(
|
||||
marker: Marker,
|
||||
tint: Int,
|
||||
gen: ChargerIconGenerator
|
||||
) {
|
||||
ValueAnimator.ofInt(20, 0).apply {
|
||||
duration = 200
|
||||
interpolator = FastOutLinearInInterpolator()
|
||||
addUpdateListener { animationState ->
|
||||
if (!marker.isVisible) {
|
||||
cancel()
|
||||
return@addUpdateListener
|
||||
fun animateMarkerAppear(
|
||||
marker: Marker,
|
||||
tint: Int
|
||||
) {
|
||||
animatingMarkers[marker]?.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
|
||||
val anim = ValueAnimator.ofInt(0, 20).apply {
|
||||
duration = 250
|
||||
interpolator = LinearOutSlowInInterpolator()
|
||||
addUpdateListener { animationState ->
|
||||
if (!marker.isVisible) {
|
||||
cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
return@addUpdateListener
|
||||
}
|
||||
val scale = animationState.animatedValue as Int
|
||||
marker.setIcon(
|
||||
gen.getBitmapDescriptor(tint, scale = scale)
|
||||
)
|
||||
}
|
||||
val scale = animationState.animatedValue as Int
|
||||
marker.setIcon(
|
||||
gen.getBitmapDescriptor(tint, scale = scale)
|
||||
)
|
||||
addListener(onEnd = {
|
||||
animatingMarkers.remove(marker)
|
||||
})
|
||||
}
|
||||
addListener(onEnd = {
|
||||
marker.remove()
|
||||
})
|
||||
}.start()
|
||||
animatingMarkers[marker] = anim
|
||||
anim.start()
|
||||
}
|
||||
|
||||
fun animateMarkerDisappear(
|
||||
marker: Marker,
|
||||
tint: Int
|
||||
) {
|
||||
animatingMarkers[marker]?.cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
|
||||
val anim = ValueAnimator.ofInt(20, 0).apply {
|
||||
duration = 200
|
||||
interpolator = FastOutLinearInInterpolator()
|
||||
addUpdateListener { animationState ->
|
||||
if (!marker.isVisible) {
|
||||
cancel()
|
||||
animatingMarkers.remove(marker)
|
||||
return@addUpdateListener
|
||||
}
|
||||
val scale = animationState.animatedValue as Int
|
||||
marker.setIcon(
|
||||
gen.getBitmapDescriptor(tint, scale = scale)
|
||||
)
|
||||
}
|
||||
addListener(onEnd = {
|
||||
animatingMarkers.remove(marker)
|
||||
marker.remove()
|
||||
})
|
||||
}
|
||||
animatingMarkers[marker] = anim
|
||||
anim.start()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.*
|
||||
import com.google.android.gms.maps.model.LatLng
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.launch
|
||||
import net.vonforst.evmap.adapter.Equatable
|
||||
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.distanceBetween
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
|
||||
class FavoritesViewModel(application: Application, geApiKey: String) :
|
||||
AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
|
||||
val favorites: LiveData<List<ChargeLocation>> by lazy {
|
||||
db.chargeLocationsDao().getAllChargeLocations()
|
||||
}
|
||||
|
||||
val location: MutableLiveData<LatLng> by lazy {
|
||||
MutableLiveData<LatLng>()
|
||||
}
|
||||
|
||||
val availability: MediatorLiveData<Map<Long, Resource<ChargeLocationStatus>>> by lazy {
|
||||
MediatorLiveData<Map<Long, Resource<ChargeLocationStatus>>>().apply {
|
||||
addSource(favorites) { chargers ->
|
||||
if (chargers != null) {
|
||||
viewModelScope.launch {
|
||||
val data = hashMapOf<Long, Resource<ChargeLocationStatus>>()
|
||||
chargers.forEach { charger ->
|
||||
data[charger.id] = Resource.loading(null)
|
||||
}
|
||||
availability.value = data
|
||||
|
||||
chargers.map { charger ->
|
||||
async {
|
||||
data[charger.id] = getAvailability(charger)
|
||||
availability.value = data
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
} else {
|
||||
value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val listData: MediatorLiveData<List<FavoritesListItem>> by lazy {
|
||||
MediatorLiveData<List<FavoritesListItem>>().apply {
|
||||
val callback = { _: Any ->
|
||||
listData.value = favorites.value?.map { charger ->
|
||||
FavoritesListItem(
|
||||
charger,
|
||||
totalAvailable(charger.id),
|
||||
charger.chargepoints.sumBy { it.count },
|
||||
location.value.let { loc ->
|
||||
if (loc == null) null else {
|
||||
distanceBetween(
|
||||
loc.latitude,
|
||||
loc.longitude,
|
||||
charger.coordinates.lat,
|
||||
charger.coordinates.lng
|
||||
) / 1000
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
addSource(favorites, callback)
|
||||
addSource(location, callback)
|
||||
addSource(availability, callback)
|
||||
}
|
||||
}
|
||||
|
||||
data class FavoritesListItem(
|
||||
val charger: ChargeLocation,
|
||||
val available: Resource<Int>,
|
||||
val total: Int,
|
||||
val distance: Double?
|
||||
) : Equatable
|
||||
|
||||
private fun totalAvailable(id: Long): Resource<Int> {
|
||||
val availability = availability.value?.get(id) ?: return Resource.error(null, null)
|
||||
if (availability.status != Status.SUCCESS) {
|
||||
return Resource(availability.status, null, availability.message)
|
||||
} else {
|
||||
val values = availability.data?.status?.values ?: return Resource.error(null, null)
|
||||
return Resource.success(values.sumBy { it.filter { it == ChargepointStatus.AVAILABLE }.size })
|
||||
}
|
||||
}
|
||||
|
||||
fun insertFavorite(charger: ChargeLocation) {
|
||||
viewModelScope.launch {
|
||||
db.chargeLocationsDao().insert(charger)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFavorite(charger: ChargeLocation) {
|
||||
viewModelScope.launch {
|
||||
db.chargeLocationsDao().delete(charger)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,25 @@
|
||||
package net.vonforst.evmap.viewmodel
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.*
|
||||
import com.google.android.gms.maps.model.LatLngBounds
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.vonforst.evmap.api.availability.AvailabilityDetectorException
|
||||
import net.vonforst.evmap.api.availability.ChargeLocationStatus
|
||||
import net.vonforst.evmap.api.availability.availabilityDetectors
|
||||
import net.vonforst.evmap.api.availability.getAvailability
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointList
|
||||
import net.vonforst.evmap.api.goingelectric.ChargepointListItem
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import net.vonforst.evmap.storage.AppDatabase
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
|
||||
|
||||
class MapViewModel(geApiKey: String) : ViewModel() {
|
||||
private var api: GoingElectricApi =
|
||||
GoingElectricApi.create(geApiKey)
|
||||
class MapViewModel(application: Application, geApiKey: String) : AndroidViewModel(application) {
|
||||
private var api = GoingElectricApi.create(geApiKey)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
|
||||
val bottomSheetState: MutableLiveData<Int> by lazy {
|
||||
MutableLiveData<Int>()
|
||||
@@ -87,6 +81,22 @@ class MapViewModel(geApiKey: String) : ViewModel() {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
|
||||
val favorites: LiveData<List<ChargeLocation>> by lazy {
|
||||
db.chargeLocationsDao().getAllChargeLocations()
|
||||
}
|
||||
|
||||
fun insertFavorite(charger: ChargeLocation) {
|
||||
viewModelScope.launch {
|
||||
db.chargeLocationsDao().insert(charger)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFavorite(charger: ChargeLocation) {
|
||||
viewModelScope.launch {
|
||||
db.chargeLocationsDao().delete(charger)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadChargepoints(mapPosition: MapPosition) {
|
||||
chargepoints.value = Resource.loading(chargepoints.value?.data)
|
||||
val bounds = mapPosition.bounds
|
||||
@@ -118,25 +128,7 @@ class MapViewModel(geApiKey: String) : ViewModel() {
|
||||
|
||||
private suspend fun loadAvailability(charger: ChargeLocation) {
|
||||
availability.value = Resource.loading(null)
|
||||
var value: Resource<ChargeLocationStatus>? = null
|
||||
withContext(Dispatchers.IO) {
|
||||
for (ad in availabilityDetectors) {
|
||||
try {
|
||||
value = Resource.success(ad.getAvailability(charger))
|
||||
break
|
||||
} catch (e: IOException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: HttpException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
} catch (e: AvailabilityDetectorException) {
|
||||
value = Resource.error(e.message, null)
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
availability.value = value
|
||||
availability.value = getAvailability(charger)
|
||||
}
|
||||
|
||||
private fun loadChargerDetails(charger: ChargeLocation) {
|
||||
|
||||
40
app/src/main/res/drawable/ic_connector_typ1.xml
Normal file
40
app/src/main/res/drawable/ic_connector_typ1.xml
Normal file
@@ -0,0 +1,40 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.79,8.26m-1.89,0a1.89,1.89 0,1 1,3.78 0a1.89,1.89 0,1 1,-3.78 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16.74,14m-1.18,0a1.18,1.18 0,1 1,2.36 0a1.18,1.18 0,1 1,-2.36 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7.26,14m-1.18,0a1.18,1.18 0,1 1,2.36 0a1.18,1.18 0,1 1,-2.36 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M8.21,8.26m-1.89,0a1.89,1.89 0,1 1,3.78 0a1.89,1.89 0,1 1,-3.78 0" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M12,17.74m-1.89,0a1.89,1.89 0,1 1,3.78 0a1.89,1.89 0,1 1,-3.78 0" />
|
||||
<path
|
||||
android:pathData="M12,12.05m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.58,21.05h2.84v1.89h-2.84z" />
|
||||
<path
|
||||
android:pathData="M10.5,1L13.5,1"
|
||||
android:strokeWidth="0.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13.5,0.4l0,2.1l2,1l-1,-3l-1,-0.1z" />
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10.5,0.4l0,2.1l-2,1l1,-3l1,-0.1z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_fav_no.xml
Normal file
10
app/src/main/res/drawable/ic_fav_no.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16.5,3c-1.74,0 -3.41,0.81 -4.5,2.09C10.91,3.81 9.24,3 7.5,3 4.42,3 2,5.42 2,8.5c0,3.78 3.4,6.86 8.55,11.54L12,21.35l1.45,-1.32C18.6,15.36 22,12.28 22,8.5 22,5.42 19.58,3 16.5,3zM12.1,18.55l-0.1,0.1 -0.1,-0.1C7.14,14.24 4,11.39 4,8.5 4,6.5 5.5,5 7.5,5c1.54,0 3.04,0.99 3.57,2.36h1.87C13.46,5.99 14.96,5 16.5,5c2,0 3.5,1.5 3.5,3.5 0,2.89 -3.14,5.74 -7.9,10.05z" />
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_share.xml
Normal file
10
app/src/main/res/drawable/ic_share.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z" />
|
||||
</vector>
|
||||
@@ -30,7 +30,7 @@
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="6dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
||||
38
app/src/main/res/layout/fragment_favorites.xml
Normal file
38
app/src/main/res/layout/fragment_favorites.xml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.viewmodel.FavoritesViewModel" />
|
||||
|
||||
<variable
|
||||
name="vm"
|
||||
type="FavoritesViewModel" />
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:actionBarSize" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/favs_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:data="@{vm.listData}" />
|
||||
</LinearLayout>
|
||||
</layout>
|
||||
@@ -27,13 +27,13 @@
|
||||
tools:context=".MapsActivity" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_behavior="@string/ScrollingAppBarLayoutBehavior">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
@@ -72,8 +72,9 @@
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/gallery_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/gallery_height"
|
||||
android:layout_height="@dimen/gallery_height_with_margin"
|
||||
android:background="?android:colorBackground"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_behavior="@string/BackDropBottomSheetBehavior">
|
||||
@@ -81,7 +82,7 @@
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/gallery"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/gallery_height"
|
||||
android:layout_height="match_parent"
|
||||
app:data="@{vm.charger.data.photos}" />
|
||||
|
||||
<ImageView
|
||||
@@ -141,10 +142,11 @@
|
||||
app:layout_anchorGravity="top|right|end"
|
||||
app:layout_behavior="@string/ScrollAwareFABBehavior" />
|
||||
|
||||
<!--<com.mahc.custombottomsheetbehavior.MergedAppBarLayout
|
||||
android:id="@+id/mergedappbarlayout"
|
||||
<com.mahc.custombottomsheetbehavior.MergedAppBarLayout
|
||||
android:id="@+id/detail_app_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_behavior="@string/MergedAppBarLayoutBehavior"/>-->
|
||||
app:layout_behavior="@string/MergedAppBarLayoutBehavior"
|
||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</layout>
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<import type="android.text.util.Linkify" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
@@ -54,7 +55,7 @@
|
||||
android:layout_marginBottom="14dp"
|
||||
android:text="@{item.detailText}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:autoLink="phone|web"
|
||||
android:autoLink="@{item.links ? Linkify.WEB_URLS | Linkify.PHONE_NUMBERS : 0}"
|
||||
app:goneUnless="@{item.detailText != null}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
||||
90
app/src/main/res/layout/item_favorite.xml
Normal file
90
app/src/main/res/layout/item_favorite.xml
Normal file
@@ -0,0 +1,90 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="net.vonforst.evmap.api.UtilsKt" />
|
||||
<import type="net.vonforst.evmap.viewmodel.Status" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="net.vonforst.evmap.viewmodel.FavoritesViewModel.FavoritesListItem" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView15"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{item.charger.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Parkhaus" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{item.charger.address.toString()}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView15"
|
||||
tools:text="Beispielstraße 10, 12345 Berlin" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:text="@{item.charger.formatChargepoints()}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintStart_toStartOf="@+id/textView2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textView2"
|
||||
tools:text="2x Typ 2 22 kW" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView16"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@{@string/distance_format(item.distance)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="9999,9 km" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView7"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/rounded_rect"
|
||||
android:padding="2dp"
|
||||
android:text="@{String.format("%d/%d", item.available.data, item.total)}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTintAvailability="@{item.available.data}"
|
||||
app:goneUnless="@{item.available.status == Status.SUCCESS}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:backgroundTint="@color/available"
|
||||
tools:text="80/99" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar4"
|
||||
style="?android:attr/progressBarStyle"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
app:goneUnless="@{item.available.status == Status.LOADING}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
16
app/src/main/res/menu/detail.xml
Normal file
16
app/src/main/res/menu/detail.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_share"
|
||||
android:icon="@drawable/ic_share"
|
||||
android:title="@string/share"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_fav"
|
||||
android:icon="@drawable/ic_fav_no"
|
||||
android:title="@string/fav_add"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
@@ -12,7 +12,7 @@
|
||||
tools:layout="@layout/fragment_map">
|
||||
<action
|
||||
android:id="@+id/action_map_to_galleryFragment"
|
||||
app:destination="@id/galleryFragment"
|
||||
app:destination="@id/gallery"
|
||||
app:enterAnim="@anim/fragment_fade_enter"
|
||||
app:exitAnim="@anim/fragment_fade_exit"
|
||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
||||
@@ -21,9 +21,21 @@
|
||||
<fragment
|
||||
android:id="@+id/about"
|
||||
android:name="net.vonforst.evmap.fragment.AboutFragment"
|
||||
android:label="@string/about" />
|
||||
android:label="@string/about"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
android:id="@+id/galleryFragment"
|
||||
android:id="@+id/settings"
|
||||
android:name="net.vonforst.evmap.fragment.SettingsFragment"
|
||||
android:label="@string/settings"
|
||||
tools:layout="@layout/fragment_preference" />
|
||||
<fragment
|
||||
android:id="@+id/gallery"
|
||||
android:name="net.vonforst.evmap.fragment.GalleryFragment"
|
||||
android:label="GalleryFragment"></fragment>
|
||||
android:label="GalleryFragment"
|
||||
tools:layout="@layout/fragment_gallery" />
|
||||
<fragment
|
||||
android:id="@+id/favs"
|
||||
android:name="net.vonforst.evmap.fragment.FavoritesFragment"
|
||||
android:label="@string/menu_favs"
|
||||
tools:layout="@layout/fragment_favorites" />
|
||||
</navigation>
|
||||
@@ -36,4 +36,12 @@
|
||||
<string name="copyright_summary">©2020 Johan von Forstner</string>
|
||||
<string name="other">Sonstiges</string>
|
||||
<string name="privacy">Datenschutzerklärung</string>
|
||||
<string name="fav_add">Zu Favoriten hinzufügen</string>
|
||||
<string name="fav_remove">Aus Favoriten entfernen</string>
|
||||
<string name="distance_format">%.1f km</string>
|
||||
<string name="pref_navigate_use_maps">Navigation sofort starten</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigationsbutton startet Navigation direkt</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigationsbutton startet Karten-App mit Position der Ladesäule</string>
|
||||
<string name="coordinates">Koordinaten</string>
|
||||
<string name="share">Teilen</string>
|
||||
</resources>
|
||||
@@ -2,4 +2,5 @@
|
||||
<resources>
|
||||
<dimen name="peek_height">72dp</dimen>
|
||||
<dimen name="gallery_height">200dp</dimen>
|
||||
<dimen name="gallery_height_with_margin">208dp</dimen>
|
||||
</resources>
|
||||
@@ -35,4 +35,12 @@
|
||||
<string name="copyright_summary">©2020 Johan von Forstner</string>
|
||||
<string name="other">Other</string>
|
||||
<string name="privacy">Privacy Notice</string>
|
||||
<string name="fav_add">Add to favorites</string>
|
||||
<string name="fav_remove">Remove from favorites</string>
|
||||
<string name="distance_format">%.1f km</string>
|
||||
<string name="pref_navigate_use_maps">Start navigation immediately</string>
|
||||
<string name="pref_navigate_use_maps_on">Navigation button starts navigation immediately</string>
|
||||
<string name="pref_navigate_use_maps_off">Navigation button launches maps app with charger location</string>
|
||||
<string name="coordinates">Coordinates</string>
|
||||
<string name="share">Share</string>
|
||||
</resources>
|
||||
|
||||
15
app/src/main/res/xml/settings.xml
Normal file
15
app/src/main/res/xml/settings.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory android:title="@string/settings">
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="navigate_use_maps"
|
||||
android:title="@string/pref_navigate_use_maps"
|
||||
android:summaryOn="@string/pref_navigate_use_maps_on"
|
||||
android:summaryOff="@string/pref_navigate_use_maps_off"
|
||||
android:defaultValue="true" />
|
||||
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
||||
12
app/src/test/java/net/vonforst/evmap/api/UtilsTest.kt
Normal file
12
app/src/test/java/net/vonforst/evmap/api/UtilsTest.kt
Normal file
@@ -0,0 +1,12 @@
|
||||
package net.vonforst.evmap.api
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
|
||||
class UtilsTest {
|
||||
@Test
|
||||
fun testDistanceBetween() {
|
||||
assertEquals(129412.71, distanceBetween(54.0, 9.0, 53.0, 8.0), 0.01)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import net.vonforst.evmap.api.goingelectric.Chargepoint
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class AvailabilityDetectorTest {
|
||||
@Test
|
||||
fun testMatchChargepointsSingleCorrect() {
|
||||
// single charger with 2 22kW chargepoints
|
||||
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
|
||||
|
||||
// correct data in NewMotion
|
||||
assertEquals(
|
||||
mapOf(chargepoints[0] to setOf(0L, 1L)),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(0L to (22.0 to "Typ2"), 1L to (22.0 to "Typ2")),
|
||||
chargepoints
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMatchChargepointsSingleWrongPower() {
|
||||
// single charger with 2 22kW chargepoints
|
||||
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
|
||||
|
||||
// wrong power in NewMotion
|
||||
assertEquals(
|
||||
mapOf(chargepoints[0] to setOf(0L, 1L)),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2")),
|
||||
chargepoints
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = AvailabilityDetectorException::class)
|
||||
fun testMatchChargepointsSingleWrong() {
|
||||
// single charger with 2 22kW chargepoints
|
||||
val chargepoints = listOf(Chargepoint("Typ2", 22.0, 2))
|
||||
|
||||
// non-matching data in NewMotion
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2"), 2L to (50.0 to "CCS")),
|
||||
chargepoints
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMatchChargepointsComplex() {
|
||||
// charger with many different connectors
|
||||
val chargepoints = listOf(
|
||||
Chargepoint("Typ2", 43.0, 1),
|
||||
Chargepoint("CCS", 50.0, 1),
|
||||
Chargepoint("CHAdeMO", 50.0, 2),
|
||||
Chargepoint("CCS", 160.0, 1),
|
||||
Chargepoint("CCS", 320.0, 2)
|
||||
)
|
||||
|
||||
// partly wrong power in NewMotion
|
||||
assertEquals(
|
||||
mapOf(
|
||||
chargepoints[0] to setOf(6L),
|
||||
chargepoints[1] to setOf(4L),
|
||||
chargepoints[2] to setOf(0L, 5L),
|
||||
chargepoints[3] to setOf(2L),
|
||||
chargepoints[4] to setOf(1L, 3L)
|
||||
),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(
|
||||
// CHAdeMO + CCS HPC
|
||||
0L to (50.0 to "CHAdeMO"),
|
||||
1L to (200.0 to "CCS"),
|
||||
// dual CCS HPC
|
||||
2L to (80.0 to "CCS"),
|
||||
3L to (200.0 to "CCS"),
|
||||
// 50kW triple charger
|
||||
4L to (50.0 to "CCS"),
|
||||
5L to (50.0 to "CHAdeMO"),
|
||||
6L to (43.0 to "Typ2")
|
||||
),
|
||||
chargepoints
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMatchChargepointsDifferentPower() {
|
||||
// single charger with 1 22kW and 1 11kW chargepoint (common when load balancing is applied)
|
||||
val chargepoints = listOf(
|
||||
Chargepoint("Typ2", 22.0, 1),
|
||||
Chargepoint("Typ2", 11.0, 1)
|
||||
)
|
||||
|
||||
// both have 27 kW power in NewMotion
|
||||
assertEquals(
|
||||
mapOf(chargepoints[1] to setOf(0L), chargepoints[0] to setOf(1L)),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(0L to (27.0 to "Typ2"), 1L to (27.0 to "Typ2")),
|
||||
chargepoints
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMatchChargepointsDifferentPower2() {
|
||||
// two chargers with 1 22kW and 1 11kW chargepoint (common when load balancing is applied)
|
||||
val chargepoints = listOf(
|
||||
Chargepoint("Typ2", 22.0, 2),
|
||||
Chargepoint("Typ2", 11.0, 2)
|
||||
)
|
||||
|
||||
// both have 27 kW power in NewMotion
|
||||
assertEquals(
|
||||
mapOf(chargepoints[1] to setOf(0L, 1L), chargepoints[0] to setOf(2L, 3L)),
|
||||
BaseAvailabilityDetector.matchChargepoints(
|
||||
mapOf(
|
||||
0L to (27.0 to "Typ2"),
|
||||
1L to (27.0 to "Typ2"),
|
||||
2L to (27.0 to "Typ2"),
|
||||
3L to (27.0 to "Typ2")
|
||||
),
|
||||
chargepoints
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package net.vonforst.evmap
|
||||
package net.vonforst.evmap.api.availability
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import net.vonforst.evmap.api.availability.NewMotionAvailabilityDetector
|
||||
import net.vonforst.evmap.api.goingelectric.ChargeLocation
|
||||
import net.vonforst.evmap.api.goingelectric.GoingElectricApi
|
||||
import okhttp3.OkHttpClient
|
||||
Reference in New Issue
Block a user