Compare commits

..

10 Commits
0.1.5 ... 0.1.7

Author SHA1 Message Date
Johan von Forstner
75b2240247 Release 0.1.7 2020-06-14 19:21:19 +02:00
Johan von Forstner
d8f011b64b Add error message when internet is not available 2020-06-14 19:19:27 +02:00
Johan von Forstner
a1760a35ff Fix startkey in GE API 2020-06-14 17:48:40 +02:00
Johan von Forstner
e5e5f8ef3c Release 0.1.6 2020-06-14 12:34:36 +02:00
Johan von Forstner
b5a4fe2dc8 Improve filter by number of chargers
- load more pages of GE results
- If server-side clustering is not available, apply local Clustering
2020-06-14 12:33:05 +02:00
Johan von Forstner
676e703a52 upgrade to Google Maps SDK v3 Beta (seems to fix #25) 2020-06-14 11:55:44 +02:00
Johan von Forstner
b9997cbb5a fix exiting with back button 2020-06-13 23:06:45 +02:00
Johan von Forstner
2558052f4f fix charge card filter 2020-06-13 22:58:53 +02:00
Johan von Forstner
980c8cc0af enable Stetho only in debug builds 2020-06-13 22:58:41 +02:00
Johan von Forstner
ffb6740da8 Add language chooser (fixes #24) 2020-06-13 19:52:39 +02:00
25 changed files with 285 additions and 55 deletions

View File

@@ -13,8 +13,8 @@ android {
applicationId "net.vonforst.evmap"
minSdkVersion 21
targetSdkVersion 29
versionCode 13
versionName "0.1.5"
versionCode 15
versionName "0.1.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -87,11 +87,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.browser:browser:1.2.0'
implementation 'com.google.maps.android:android-maps-utils:0.5'
implementation 'com.github.johan12345:CustomBottomSheetBehavior:f69f532660'
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.3.0'
implementation 'com.squareup.retrofit2:retrofit:2.7.2'
implementation 'com.squareup.retrofit2:converter-moshi:2.7.2'
implementation 'com.squareup.moshi:moshi-kotlin:1.9.2'
@@ -102,6 +98,15 @@ dependencies {
implementation 'com.airbnb.android:lottie:3.4.0'
implementation 'io.michaelrocks:bimap:1.0.2'
// Google Maps v3 Beta
implementation name:'maps-sdk-3.0.0-beta', ext:'aar'
implementation name:'places-maps-sdk-3.0.0-beta', ext:'aar'
implementation 'com.google.maps.android:android-maps-utils-v3:1.3.3'
implementation 'com.google.android.gms:play-services-basement:17.3.0'
implementation 'com.google.android.gms:play-services-base:17.3.0'
implementation 'com.google.android.gms:play-services-gcm:17.0.0'
implementation 'com.google.android.gms:play-services-location:17.0.0'
// navigation library
def nav_version = "2.3.0-beta01"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
@@ -19,6 +20,7 @@ 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
import net.vonforst.evmap.utils.LocaleContextWrapper
const val REQUEST_LOCATION_PERMISSION = 1
@@ -34,6 +36,14 @@ class MapsActivity : AppCompatActivity() {
var fragmentCallback: FragmentCallback? = null
private lateinit var prefs: PreferenceDataSource
override fun attachBaseContext(newBase: Context) {
return super.attachBaseContext(
LocaleContextWrapper.wrap(
newBase, PreferenceDataSource(newBase).language
)
);
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

View File

@@ -3,6 +3,7 @@ package net.vonforst.evmap.api.goingelectric
import android.content.Context
import com.facebook.stetho.okhttp3.StethoInterceptor
import com.squareup.moshi.Moshi
import net.vonforst.evmap.BuildConfig
import okhttp3.Cache
import okhttp3.OkHttpClient
import retrofit2.Call
@@ -25,7 +26,8 @@ interface GoingElectricApi {
@Query("min_power") minPower: Int,
@Query("plugs") plugs: String?,
@Query("chargecards") chargecards: String?,
@Query("networks") networks: String?
@Query("networks") networks: String?,
@Query("startkey") startkey: Int?
): Response<ChargepointList>
@GET("chargepoints/")
@@ -56,7 +58,9 @@ interface GoingElectricApi {
original = original.newBuilder().url(url).build()
chain.proceed(original)
}
addNetworkInterceptor(StethoInterceptor())
if (BuildConfig.DEBUG) {
addNetworkInterceptor(StethoInterceptor())
}
if (context != null) {
cache(Cache(context.getCacheDir(), cacheSize))
}

View File

@@ -24,7 +24,8 @@ import kotlin.math.floor
@JsonClass(generateAdapter = true)
data class ChargepointList(
val status: String,
val chargelocations: List<ChargepointListItem>
val chargelocations: List<ChargepointListItem>,
@JsonObjectOrFalse val startkey: Int?
)
@JsonClass(generateAdapter = true)
@@ -277,4 +278,4 @@ data class ChargeCard(
@Json(name = "card_id") @PrimaryKey val id: Long,
val name: String,
val url: String
)
)

View File

@@ -18,7 +18,7 @@ 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 com.google.android.libraries.maps.model.LatLng
import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
import net.vonforst.evmap.adapter.FavoritesAdapter

View File

@@ -34,11 +34,11 @@ import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.OnMapReadyCallback
import com.google.android.gms.maps.SupportMapFragment
import com.google.android.gms.maps.model.*
import com.google.android.libraries.maps.CameraUpdateFactory
import com.google.android.libraries.maps.GoogleMap
import com.google.android.libraries.maps.OnMapReadyCallback
import com.google.android.libraries.maps.SupportMapFragment
import com.google.android.libraries.maps.model.*
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
@@ -89,6 +89,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
private var clusterMarkers: List<Marker> = emptyList()
private var searchResultMarker: Marker? = null
private var connectionErrorSnackbar: Snackbar? = null
private lateinit var clusterIconGenerator: ClusterIconGenerator
private lateinit var chargerIconGenerator: ChargerIconGenerator
@@ -312,9 +313,31 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
unhighlightAllMarkers()
}
})
vm.chargepoints.observe(viewLifecycleOwner, Observer {
val chargepoints = it.data
if (chargepoints != null) updateMap(chargepoints)
vm.chargepoints.observe(viewLifecycleOwner, Observer { res ->
when (res.status) {
Status.ERROR -> {
val view = view ?: return@Observer
connectionErrorSnackbar?.dismiss()
connectionErrorSnackbar = Snackbar
.make(view, R.string.connection_error, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.retry) {
connectionErrorSnackbar?.dismiss()
vm.reloadChargepoints()
}
connectionErrorSnackbar!!.show()
}
Status.SUCCESS -> {
connectionErrorSnackbar?.dismiss()
}
Status.LOADING -> {
}
}
val chargepoints = res.data
if (chargepoints != null) {
updateMap(chargepoints)
}
})
vm.favorites.observe(viewLifecycleOwner, Observer {
updateFavoriteToggle()
@@ -347,11 +370,14 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
vm.mapTrafficEnabled.observe(viewLifecycleOwner, Observer {
map?.isTrafficEnabled = it
})
updateBackPressedCallback()
}
private fun updateBackPressedCallback() {
backPressedCallback.isEnabled =
vm.bottomSheetState.value != STATE_HIDDEN || vm.searchResult.value != null
vm.bottomSheetState.value != null && vm.bottomSheetState.value != STATE_HIDDEN
|| vm.searchResult.value != null
|| (vm.layersMenuOpen.value ?: false)
}

View File

@@ -1,5 +1,6 @@
package net.vonforst.evmap.fragment
import android.content.SharedPreferences
import android.os.Bundle
import android.view.View
import androidx.appcompat.widget.Toolbar
@@ -11,7 +12,8 @@ import net.vonforst.evmap.MapsActivity
import net.vonforst.evmap.R
class SettingsFragment : PreferenceFragmentCompat() {
class SettingsFragment : PreferenceFragmentCompat(),
SharedPreferences.OnSharedPreferenceChangeListener {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val toolbar = view.findViewById(R.id.toolbar) as Toolbar
@@ -33,4 +35,26 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) {
when (key) {
"language" -> {
activity?.let {
it.finish();
it.startActivity(it.intent);
}
}
}
}
override fun onResume() {
super.onResume()
preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
}
override fun onPause() {
preferenceManager.sharedPreferences
.unregisterOnSharedPreferenceChangeListener(this)
super.onPause()
}
}

View File

@@ -30,4 +30,7 @@ class PreferenceDataSource(context: Context) {
set(value) {
sp.edit().putLong("last_chargecard_update", value.toEpochMilli()).apply()
}
val language: String
get() = sp.getString("language", "default")!!
}

View File

@@ -0,0 +1,41 @@
package net.vonforst.evmap.ui;
import com.google.android.libraries.maps.model.LatLng
import com.google.maps.android.clustering.ClusterItem
import com.google.maps.android.clustering.algo.NonHierarchicalDistanceBasedAlgorithm
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.api.goingelectric.Coordinate
fun cluster(
result: List<ChargepointListItem>,
zoom: Float,
clusterDistance: Int
): List<ChargepointListItem> {
val clusters = result.filterIsInstance<ChargeLocationCluster>()
val locations = result.filterIsInstance<ChargeLocation>()
val clusterItems = locations.map { ChargepointClusterItem(it) }
val algo = NonHierarchicalDistanceBasedAlgorithm<ChargepointClusterItem>()
algo.maxDistanceBetweenClusteredItems = clusterDistance
algo.addItems(clusterItems)
return algo.getClusters(zoom).map {
if (it.size == 1) {
it.items.first().charger
} else {
ChargeLocationCluster(it.size, Coordinate(it.position.latitude, it.position.longitude))
}
} + clusters
}
private class ChargepointClusterItem(val charger: ChargeLocation) : ClusterItem {
override fun getSnippet(): String? = null
override fun getTitle(): String? = charger.name
override fun getPosition(): LatLng = LatLng(charger.coordinates.lat, charger.coordinates.lng)
}

View File

@@ -11,8 +11,8 @@ import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.widget.TextViewCompat
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.libraries.maps.model.BitmapDescriptor
import com.google.android.libraries.maps.model.BitmapDescriptorFactory
import com.google.maps.android.ui.IconGenerator
import com.google.maps.android.ui.SquareTextView
import net.vonforst.evmap.R

View File

@@ -5,7 +5,7 @@ import android.view.animation.BounceInterpolator
import androidx.core.animation.addListener
import androidx.interpolator.view.animation.FastOutLinearInInterpolator
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import com.google.android.gms.maps.model.Marker
import com.google.android.libraries.maps.model.Marker
import net.vonforst.evmap.R
import net.vonforst.evmap.api.goingelectric.ChargeLocation
import kotlin.math.max

View File

@@ -0,0 +1,42 @@
package net.vonforst.evmap.utils
import android.annotation.TargetApi
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
import android.os.Build
import java.util.*
class LocaleContextWrapper(base: Context?) : ContextWrapper(base) {
companion object {
fun wrap(context: Context, language: String): ContextWrapper {
val config: Configuration = context.resources.configuration
var sysLocale: Locale? = null
sysLocale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
config.locales.get(0)
} else {
@Suppress("DEPRECATION")
config.locale
}
var ctx = context
if (language != "" && language != "default" && sysLocale.language != language) {
val locale = Locale(language)
Locale.setDefault(locale)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
config.setLocale(locale)
} else {
@Suppress("DEPRECATION")
config.locale = locale
}
ctx = context.createConfigurationContext(config)
}
return LocaleContextWrapper(ctx)
}
@TargetApi(Build.VERSION_CODES.N)
fun setSystemLocale(config: Configuration, locale: Locale?) {
config.setLocale(locale)
}
}
}

View File

@@ -2,7 +2,7 @@ package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.*
import com.google.android.gms.maps.model.LatLng
import com.google.android.libraries.maps.model.LatLng
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch

View File

@@ -61,7 +61,7 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
plug.name to (plugNames[plug.name] ?: plug.name)
}?.toMap() ?: return
val networkMap = networks.value?.map { it.name to it.name }?.toMap() ?: return
val chargecardMap = chargeCards.value?.map { it.name to it.name }?.toMap() ?: return
val chargecardMap = chargeCards.value?.map { it.id.toString() to it.name }?.toMap() ?: return
value = listOf(
BooleanFilter(application.getString(R.string.filter_free), "freecharging"),
BooleanFilter(application.getString(R.string.filter_free_parking), "freeparking"),

View File

@@ -2,18 +2,21 @@ package net.vonforst.evmap.viewmodel
import android.app.Application
import androidx.lifecycle.*
import com.google.android.gms.maps.GoogleMap
import com.google.android.gms.maps.model.LatLngBounds
import com.google.android.libraries.maps.GoogleMap
import com.google.android.libraries.maps.model.LatLngBounds
import com.google.android.libraries.places.api.model.Place
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import net.vonforst.evmap.api.availability.ChargeLocationStatus
import net.vonforst.evmap.api.availability.getAvailability
import net.vonforst.evmap.api.goingelectric.*
import net.vonforst.evmap.storage.*
import net.vonforst.evmap.ui.cluster
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
data class MapPosition(val bounds: LatLngBounds, val zoom: Float)
@@ -74,9 +77,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
value = Resource.loading(emptyList())
listOf(mapPosition, filtersWithValue).forEach {
addSource(it) {
val pos = mapPosition.value ?: return@addSource
val filters = filtersWithValue.value ?: return@addSource
loadChargepoints(pos, filters)
reloadChargepoints()
}
}
}
@@ -172,6 +173,12 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
}
}
fun reloadChargepoints() {
val pos = mapPosition.value ?: return
val filters = filtersWithValue.value ?: return
loadChargepoints(pos, filters)
}
private fun loadChargepoints(
mapPosition: MapPosition,
filters: List<FilterWithValue<out FilterValue>>
@@ -218,35 +225,63 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
val networks = formatMultipleChoice(networksVal)
// do not use clustering if filters need to be applied locally.
val useClustering = minConnectors <= 1 && zoom < 13
val useClustering = zoom < 13
val geClusteringAvailable = minConnectors <= 1
val useGeClustering = useClustering && geClusteringAvailable
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
val response = api.getChargepoints(
bounds.southwest.latitude, bounds.southwest.longitude,
bounds.northeast.latitude, bounds.northeast.longitude,
clustering = useClustering, zoom = zoom,
clusterDistance = clusterDistance, freecharging = freecharging, minPower = minPower,
freeparking = freeparking, plugs = connectors, chargecards = chargeCards,
networks = networks
)
if (!response.isSuccessful || response.body()!!.status != "ok") {
return Resource.error(response.message(), chargepoints.value?.data)
} else {
val data = response.body()!!.chargelocations.filter { it ->
// apply filters which GoingElectric does not support natively
if (it is ChargeLocation) {
it.chargepoints
.filter { it.power >= minPower }
.filter { if (!connectorsVal.all) it.type in connectorsVal.values else true }
.sumBy { it.count } >= minConnectors
var startkey: Int? = null
val data = mutableListOf<ChargepointListItem>()
do {
// load all pages of the response
try {
val response = api.getChargepoints(
bounds.southwest.latitude,
bounds.southwest.longitude,
bounds.northeast.latitude,
bounds.northeast.longitude,
clustering = useGeClustering,
zoom = zoom,
clusterDistance = clusterDistance,
freecharging = freecharging,
minPower = minPower,
freeparking = freeparking,
plugs = connectors,
chargecards = chargeCards,
networks = networks,
startkey = startkey
)
if (!response.isSuccessful || response.body()!!.status != "ok") {
return Resource.error(response.message(), chargepoints.value?.data)
} else {
true
val body = response.body()!!
data.addAll(body.chargelocations)
startkey = body.startkey
}
} catch (e: IOException) {
return Resource.error(e.message, chargepoints.value?.data)
}
} while (startkey != null && startkey < 10000)
return Resource.success(data)
var result = data.filter { it ->
// apply filters which GoingElectric does not support natively
if (it is ChargeLocation) {
it.chargepoints
.filter { it.power >= minPower }
.filter { if (!connectorsVal.all) it.type in connectorsVal.values else true }
.sumBy { it.count } >= minConnectors
} else {
true
}
}
if (!geClusteringAvailable && useClustering) {
// apply local clustering if server side clustering is not available
Dispatchers.IO.run {
result = cluster(result, zoom, clusterDistance!!)
}
}
return Resource.success(result)
}
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =

View File

@@ -21,7 +21,7 @@
<fragment
android:id="@+id/map"
android:name="com.google.android.gms.maps.SupportMapFragment"
android:name="com.google.android.libraries.maps.SupportMapFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MapsActivity" />

View File

@@ -6,7 +6,7 @@
<import type="net.vonforst.evmap.viewmodel.MapViewModel" />
<import type="com.google.android.gms.maps.GoogleMap" />
<import type="com.google.android.libraries.maps.GoogleMap" />
<variable
name="vm"

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_language_names">
<item>Gerätesprache verwenden</item>
<item>Englisch</item>
<item>Deutsch</item>
</string-array>
</resources>

View File

@@ -90,4 +90,8 @@
<string name="edit">bearbeiten</string>
<string name="cancel">Abbrechen</string>
<string name="ok">OK</string>
<string name="pref_language">Sprache</string>
<string name="pref_language_summary">App-Sprache ändern</string>
<string name="connection_error">Ladesäulen konnten nicht geladen werden</string>
<string name="retry">Wiederholen</string>
</resources>

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="pref_language_names">
<item>Device default</item>
<item>English</item>
<item>German</item>
</string-array>
<string-array name="pref_language_values" tranlatable="false">
<item>default</item>
<item>en</item>
<item>de</item>
</string-array>
</resources>

View File

@@ -89,4 +89,8 @@
<string name="edit">edit</string>
<string name="cancel">Cancel</string>
<string name="ok">OK</string>
<string name="pref_language">Language</string>
<string name="pref_language_summary">Change the app language</string>
<string name="connection_error">Could not load charging stations</string>
<string name="retry">Retry</string>
</resources>

View File

@@ -10,6 +10,13 @@
android:summaryOff="@string/pref_navigate_use_maps_off"
android:defaultValue="true" />
<ListPreference
android:key="language"
android:title="@string/pref_language"
android:entries="@array/pref_language_names"
android:entryValues="@array/pref_language_values"
android:defaultValue="default"
android:summary="@string/pref_language_summary" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -25,6 +25,9 @@ allprojects {
google()
jcenter()
maven { url 'https://jitpack.io' }
flatDir {
dirs 'libs'
}
}
}