mirror of
https://github.com/ev-map/EVMap.git
synced 2025-12-25 08:07:46 -05:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0be90d8801 | ||
|
|
4ca9cc68cb | ||
|
|
62e9acf9be | ||
|
|
6cb682f065 | ||
|
|
4cfd5c8ef2 | ||
|
|
24bf66ddbe | ||
|
|
a0b0339c8b | ||
|
|
2c9081b313 | ||
|
|
bd245801b0 | ||
|
|
11dac62b94 | ||
|
|
a8bac7875a | ||
|
|
dbba00b51b | ||
|
|
90cddce54c | ||
|
|
f0f6c08610 | ||
|
|
a2fe9a06c5 | ||
|
|
cb79f17c23 | ||
|
|
0009895537 | ||
|
|
df705670b1 | ||
|
|
c616e9fdbd |
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Johan von Forstner
|
||||
Copyright (c) 2020-2021 Johan von Forstner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -20,6 +20,7 @@ Features
|
||||
- Favorites list, also with availability information
|
||||
- No ads, fully open source
|
||||
- Compatible with Android 5.0 and above
|
||||
- Can use Google Maps or Mapbox (OpenStreetMap) as map backends - the version available on F-Droid only uses Mapbox.
|
||||
|
||||
Screenshots
|
||||
-----------
|
||||
@@ -31,10 +32,10 @@ Development setup
|
||||
|
||||
The App is developed using Android Studio.
|
||||
|
||||
For testing the app, you need to obtain API Keys for the
|
||||
For testing the app, you need to obtain free API Keys for the
|
||||
[GoingElectric API](https://www.goingelectric.de/stromtankstellen/api/)
|
||||
as well as for [Google APIs](https://console.developers.google.com/)
|
||||
("Maps SDK for Android" and "Places API" need to be activated). These APIs need to be put into the
|
||||
("Maps SDK for Android" and "Places API" need to be activated) and/or [Mapbox](https://www.mapbox.com/). These APIs need to be put into the
|
||||
app in the form of a resource file called `apikeys.xml` under `app/src/main/res/values`, with the
|
||||
following content:
|
||||
|
||||
@@ -43,6 +44,9 @@ following content:
|
||||
<string name="google_maps_key" templateMergeStrategy="preserve" translatable="false">
|
||||
insert your Google Maps key here
|
||||
</string>
|
||||
<string name="mapbox_key" translatable="false">
|
||||
insert your Mapbox key here
|
||||
</string>
|
||||
<string name="goingelectric_key" translatable="false">
|
||||
insert your GoingElectric key here
|
||||
</string>
|
||||
|
||||
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "net.vonforst.evmap"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
versionCode 34
|
||||
versionName "0.4.2"
|
||||
versionCode 39
|
||||
versionName "0.5.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
@@ -120,7 +120,7 @@ dependencies {
|
||||
implementation 'com.github.pengrad:mapscaleview:1.6.0'
|
||||
|
||||
// AnyMaps
|
||||
def anyMapsVersion = '7753eeb7b0'
|
||||
def anyMapsVersion = '1f050d860f'
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-base:$anyMapsVersion"
|
||||
googleImplementation "com.github.johan12345.AnyMaps:anymaps-google:$anyMapsVersion"
|
||||
implementation "com.github.johan12345.AnyMaps:anymaps-mapbox:$anyMapsVersion"
|
||||
|
||||
@@ -38,6 +38,21 @@
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="geo" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.goingelectric.de"
|
||||
android:pathPrefix="/stromtankstellen/" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
||||
@@ -17,6 +17,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.fragment.MapFragment
|
||||
import net.vonforst.evmap.storage.PreferenceDataSource
|
||||
import net.vonforst.evmap.utils.LocaleContextWrapper
|
||||
|
||||
@@ -64,6 +65,34 @@ class MapsActivity : AppCompatActivity() {
|
||||
prefs = PreferenceDataSource(this)
|
||||
|
||||
checkPlayServices(this)
|
||||
|
||||
if (intent?.scheme == "geo") {
|
||||
val pos = intent.data?.schemeSpecificPart?.split("?")?.get(0)
|
||||
val coords = pos?.split(",")?.map { it.toDoubleOrNull() }
|
||||
|
||||
if (coords != null && coords.size == 2) {
|
||||
val lat = coords[0]
|
||||
val lon = coords[1]
|
||||
if (lat != null && lon != null) {
|
||||
val deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragment.showLocation(lat, lon))
|
||||
.createPendingIntent()
|
||||
deepLink.send()
|
||||
}
|
||||
}
|
||||
} else if (intent?.scheme == "https" && intent?.data?.host == "www.goingelectric.de") {
|
||||
val id = intent.data?.pathSegments?.last()?.toLongOrNull()
|
||||
if (id != null) {
|
||||
val deepLink = navController.createDeepLink()
|
||||
.setGraph(R.navigation.nav_graph)
|
||||
.setDestination(R.id.map)
|
||||
.setArguments(MapFragment.showChargerById(id))
|
||||
.createPendingIntent()
|
||||
deepLink.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateTo(charger: ChargeLocation) {
|
||||
|
||||
@@ -8,7 +8,10 @@ import net.vonforst.evmap.api.RateLimitInterceptor
|
||||
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.FilterValues
|
||||
import net.vonforst.evmap.viewmodel.Resource
|
||||
import net.vonforst.evmap.viewmodel.getMultipleChoiceValue
|
||||
import net.vonforst.evmap.viewmodel.getSliderValue
|
||||
import okhttp3.JavaNetCookieJar
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
@@ -113,7 +116,21 @@ abstract class BaseAvailabilityDetector(private val client: OkHttpClient) : Avai
|
||||
data class ChargeLocationStatus(
|
||||
val status: Map<Chargepoint, List<ChargepointStatus>>,
|
||||
val source: String
|
||||
)
|
||||
) {
|
||||
fun applyFilters(filters: FilterValues?): ChargeLocationStatus {
|
||||
if (filters == null) return this
|
||||
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")
|
||||
val minPower = filters.getSliderValue("min_power")
|
||||
|
||||
val statusFiltered = status.filterKeys {
|
||||
(connectorsVal.all || it.type in connectorsVal.values) && it.power > minPower
|
||||
}
|
||||
return this.copy(status = statusFiltered)
|
||||
}
|
||||
|
||||
val totalChargepoints = status.map { it.key.count }.sum()
|
||||
}
|
||||
|
||||
enum class ChargepointStatus {
|
||||
AVAILABLE, UNKNOWN, CHARGING, OCCUPIED, FAULTED
|
||||
|
||||
@@ -86,8 +86,9 @@ class FavoritesFragment : Fragment(), LostApiClient.ConnectionCallbacks {
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
val context = this.context ?: return
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
context,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
|
||||
@@ -33,6 +33,8 @@ import net.vonforst.evmap.viewmodel.viewModelFactory
|
||||
|
||||
|
||||
class FilterProfilesFragment : Fragment() {
|
||||
private lateinit var touchHelper: ItemTouchHelper
|
||||
private lateinit var adapter: FilterProfilesAdapter
|
||||
private lateinit var binding: FragmentFilterProfilesBinding
|
||||
private val vm: FilterProfilesViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
@@ -40,6 +42,7 @@ class FilterProfilesFragment : Fragment() {
|
||||
}
|
||||
})
|
||||
private var deleteSnackbar: Snackbar? = null
|
||||
private var toDelete: FilterProfile? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
@@ -64,7 +67,7 @@ class FilterProfilesFragment : Fragment() {
|
||||
)
|
||||
|
||||
|
||||
val touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
||||
touchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.UP or ItemTouchHelper.DOWN,
|
||||
ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT
|
||||
) {
|
||||
@@ -173,7 +176,7 @@ class FilterProfilesFragment : Fragment() {
|
||||
}
|
||||
})
|
||||
|
||||
val adapter = FilterProfilesAdapter(touchHelper, onDelete = { fp ->
|
||||
adapter = FilterProfilesAdapter(touchHelper, onDelete = { fp ->
|
||||
delete(fp)
|
||||
}, onRename = { fp ->
|
||||
showEditTextDialog(requireContext()) { dialog, input ->
|
||||
@@ -183,7 +186,7 @@ class FilterProfilesFragment : Fragment() {
|
||||
.setMessage(R.string.save_profile_enter_name)
|
||||
.setPositiveButton(R.string.ok) { di, button ->
|
||||
lifecycleScope.launch {
|
||||
vm.insert(fp.copy(name = input.text.toString()))
|
||||
vm.update(fp.copy(name = input.text.toString()))
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { di, button ->
|
||||
@@ -192,7 +195,7 @@ class FilterProfilesFragment : Fragment() {
|
||||
}
|
||||
})
|
||||
binding.filterProfilesList.apply {
|
||||
this.adapter = adapter
|
||||
this.adapter = this@FilterProfilesFragment.adapter
|
||||
layoutManager =
|
||||
LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
||||
addItemDecoration(
|
||||
@@ -210,19 +213,43 @@ class FilterProfilesFragment : Fragment() {
|
||||
}
|
||||
|
||||
fun delete(fp: FilterProfile) {
|
||||
vm.delete(fp.id)
|
||||
|
||||
val position = vm.filterProfiles.value?.indexOf(fp) ?: return
|
||||
// if there is already a profile to delete, delete it now
|
||||
actuallyDelete()
|
||||
deleteSnackbar?.dismiss()
|
||||
|
||||
toDelete = fp
|
||||
|
||||
view?.let {
|
||||
val snackbar = Snackbar.make(
|
||||
it,
|
||||
getString(R.string.deleted_filterprofile, fp.name),
|
||||
Snackbar.LENGTH_LONG
|
||||
).setAction(R.string.undo) {
|
||||
vm.insert(fp.copy(id = 0))
|
||||
}
|
||||
toDelete = null
|
||||
adapter.notifyItemChanged(position)
|
||||
}.addCallback(object : Snackbar.Callback() {
|
||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
||||
// if undo was not clicked, actually delete
|
||||
if (event == DISMISS_EVENT_TIMEOUT || event == DISMISS_EVENT_SWIPE) {
|
||||
actuallyDelete()
|
||||
}
|
||||
}
|
||||
})
|
||||
deleteSnackbar = snackbar
|
||||
snackbar.show()
|
||||
} ?: run {
|
||||
actuallyDelete()
|
||||
}
|
||||
}
|
||||
|
||||
private fun actuallyDelete() {
|
||||
toDelete?.let { vm.delete(it.id) }
|
||||
toDelete = null
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
actuallyDelete()
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.location.Location
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.view.*
|
||||
@@ -50,6 +51,8 @@ import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN
|
||||
import com.mahc.custombottomsheetbehavior.MergedAppBarLayoutBehavior
|
||||
import com.mapzen.android.lost.api.LocationListener
|
||||
import com.mapzen.android.lost.api.LocationRequest
|
||||
import com.mapzen.android.lost.api.LocationServices
|
||||
import com.mapzen.android.lost.api.LostApiClient
|
||||
import io.michaelrocks.bimap.HashBiMap
|
||||
@@ -74,13 +77,14 @@ import net.vonforst.evmap.ui.MarkerAnimator
|
||||
import net.vonforst.evmap.ui.getMarkerTint
|
||||
import net.vonforst.evmap.viewmodel.*
|
||||
|
||||
|
||||
const val REQUEST_AUTOCOMPLETE = 2
|
||||
const val ARG_CHARGER_ID = "chargerId"
|
||||
const val ARG_LAT = "lat"
|
||||
const val ARG_LON = "lon"
|
||||
|
||||
class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallback,
|
||||
LostApiClient.ConnectionCallbacks {
|
||||
LostApiClient.ConnectionCallbacks, LocationListener {
|
||||
private lateinit var binding: FragmentMapBinding
|
||||
private val vm: MapViewModel by viewModels(factoryProducer = {
|
||||
viewModelFactory {
|
||||
@@ -94,6 +98,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private var mapFragment: MapFragment? = null
|
||||
private var map: AnyMap? = null
|
||||
private lateinit var locationClient: LostApiClient
|
||||
private var requestingLocationUpdates = false
|
||||
private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike<View>
|
||||
private lateinit var detailAppBarBehavior: MergedAppBarLayoutBehavior
|
||||
private var markers: MutableBiMap<Marker, ChargeLocation> = HashBiMap()
|
||||
@@ -224,7 +229,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
)
|
||||
|
||||
if (!PreferenceDataSource(requireContext()).welcomeDialogShown) {
|
||||
navController.navigate(R.id.action_map_to_welcome)
|
||||
try {
|
||||
navController.navigate(R.id.action_map_to_welcome)
|
||||
} catch (ignored: IllegalArgumentException) {
|
||||
// when there is already another navigation going on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +242,13 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val hostActivity = activity as? MapsActivity ?: return
|
||||
hostActivity.fragmentCallback = this
|
||||
vm.reloadPrefs()
|
||||
if (requestingLocationUpdates && ContextCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
requestLocationUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupClickListeners() {
|
||||
@@ -270,7 +286,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
binding.detailView.btnChargeprice.setOnClickListener {
|
||||
val charger = vm.charger.value?.data ?: return@setOnClickListener
|
||||
(activity as? MapsActivity)?.openUrl(
|
||||
"https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric")
|
||||
"https://www.chargeprice.app/?poi_id=${charger.id}&poi_source=going_electric"
|
||||
)
|
||||
}
|
||||
binding.detailView.topPart.setOnClickListener {
|
||||
bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT
|
||||
@@ -353,7 +370,12 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
vm.bottomSheetState.value = newState
|
||||
updateBackPressedCallback()
|
||||
|
||||
if (vm.layersMenuOpen.value!! && newState !in listOf(BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING, BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN, BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED)) {
|
||||
if (vm.layersMenuOpen.value!! && newState !in listOf(
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_SETTLING,
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN,
|
||||
BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED
|
||||
)
|
||||
) {
|
||||
closeLayersMenu()
|
||||
}
|
||||
}
|
||||
@@ -673,6 +695,15 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
map.setOnCameraMoveListener {
|
||||
binding.scaleView.update(map.cameraPosition.zoom, map.cameraPosition.target.latitude)
|
||||
}
|
||||
map.setOnCameraMoveStartedListener { reason ->
|
||||
if (reason == AnyMap.OnCameraMoveStartedListener.REASON_GESTURE
|
||||
&& vm.myLocationEnabled.value == true
|
||||
) {
|
||||
// disable location following when manually scrolling the map
|
||||
vm.myLocationEnabled.value = false
|
||||
removeLocationUpdates()
|
||||
}
|
||||
}
|
||||
map.setOnMarkerClickListener { marker ->
|
||||
when (marker) {
|
||||
in markers -> {
|
||||
@@ -698,6 +729,7 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
backPressedCallback.handleOnBackPressed()
|
||||
}
|
||||
}
|
||||
map.setMapType(vm.mapType.value)
|
||||
|
||||
// set padding so that compass is not obstructed by toolbar
|
||||
map.setPadding(0, binding.toolbarContainer.height, 0, 0)
|
||||
@@ -711,33 +743,57 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
val position = vm.mapPosition.value
|
||||
val lat = arguments?.optDouble(ARG_LAT)
|
||||
val lon = arguments?.optDouble(ARG_LON)
|
||||
val chargerId = arguments?.optLong(ARG_CHARGER_ID)
|
||||
|
||||
var positionSet = false
|
||||
|
||||
if (position != null) {
|
||||
val cameraUpdate =
|
||||
map.cameraUpdateFactory.newLatLngZoom(position.bounds.center, position.zoom)
|
||||
map.moveCamera(cameraUpdate)
|
||||
positionSet = true
|
||||
} else if (chargerId != null && (lat == null || lon == null)) {
|
||||
// show given charger ID
|
||||
vm.loadChargerById(chargerId)
|
||||
vm.chargerSparse.observe(
|
||||
viewLifecycleOwner,
|
||||
object : Observer<ChargeLocation> {
|
||||
override fun onChanged(item: ChargeLocation?) {
|
||||
if (item?.id == chargerId) {
|
||||
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(
|
||||
LatLng(item.coordinates.lat, item.coordinates.lng), 16f
|
||||
)
|
||||
map.moveCamera(cameraUpdate)
|
||||
vm.chargerSparse.removeObserver(this)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
positionSet = true
|
||||
} else if (lat != null && lon != null) {
|
||||
// show given position
|
||||
val cameraUpdate = map.cameraUpdateFactory.newLatLngZoom(LatLng(lat, lon), 16f)
|
||||
map.moveCamera(cameraUpdate)
|
||||
|
||||
// show charger detail after chargers were loaded
|
||||
val chargerId = arguments?.optLong(ARG_CHARGER_ID)
|
||||
vm.chargepoints.observe(
|
||||
viewLifecycleOwner,
|
||||
object : Observer<Resource<List<ChargepointListItem>>> {
|
||||
override fun onChanged(res: Resource<List<ChargepointListItem>>) {
|
||||
if (res.data == null) return
|
||||
for (item in res.data) {
|
||||
if (item is ChargeLocation && item.id == chargerId) {
|
||||
vm.chargerSparse.value = item
|
||||
vm.chargepoints.removeObserver(this)
|
||||
if (chargerId != null) {
|
||||
// show charger detail after chargers were loaded
|
||||
vm.chargepoints.observe(
|
||||
viewLifecycleOwner,
|
||||
object : Observer<Resource<List<ChargepointListItem>>> {
|
||||
override fun onChanged(res: Resource<List<ChargepointListItem>>) {
|
||||
if (res.data == null) return
|
||||
for (item in res.data) {
|
||||
if (item is ChargeLocation && item.id == chargerId) {
|
||||
vm.chargerSparse.value = item
|
||||
vm.chargepoints.removeObserver(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// mark location as search result
|
||||
vm.searchResult.value = PlaceWithBounds(LatLng(lat, lon), null)
|
||||
}
|
||||
|
||||
positionSet = true
|
||||
}
|
||||
@@ -765,15 +821,18 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
private fun enableLocation(moveTo: Boolean, animate: Boolean) {
|
||||
val map = this.map ?: return
|
||||
map.setMyLocationEnabled(true)
|
||||
vm.myLocationEnabled.value = true
|
||||
map.uiSettings.setMyLocationButtonEnabled(false)
|
||||
if (moveTo && locationClient.isConnected) {
|
||||
moveToCurrentLocation(map, animate)
|
||||
if (moveTo) {
|
||||
vm.myLocationEnabled.value = true
|
||||
if (locationClient.isConnected) {
|
||||
moveToLastLocation(map, animate)
|
||||
requestLocationUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(ACCESS_FINE_LOCATION)
|
||||
private fun moveToCurrentLocation(map: AnyMap, animate: Boolean) {
|
||||
private fun moveToLastLocation(map: AnyMap, animate: Boolean) {
|
||||
val location = LocationServices.FusedLocationApi.getLastLocation(locationClient)
|
||||
if (location != null) {
|
||||
val latLng = LatLng(location.latitude, location.longitude)
|
||||
@@ -1050,21 +1109,64 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
|
||||
putDouble(ARG_LON, charger.coordinates.lng)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
val map = this.map ?: return
|
||||
if (vm.myLocationEnabled.value == true) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
requireContext(),
|
||||
ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
moveToCurrentLocation(map, false)
|
||||
fun showLocation(lat: Double, lon: Double): Bundle {
|
||||
return Bundle().apply {
|
||||
putDouble(ARG_LAT, lat)
|
||||
putDouble(ARG_LON, lon)
|
||||
}
|
||||
}
|
||||
|
||||
fun showChargerById(id: Long): Bundle? {
|
||||
return Bundle().apply {
|
||||
putLong(ARG_CHARGER_ID, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConnected() {
|
||||
val map = this.map ?: return
|
||||
val context = this.context ?: return
|
||||
if (vm.myLocationEnabled.value == true) {
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
ACCESS_FINE_LOCATION
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
moveToLastLocation(map, false)
|
||||
requestLocationUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresPermission(ACCESS_FINE_LOCATION)
|
||||
private fun requestLocationUpdates() {
|
||||
val request: LocationRequest = LocationRequest.create()
|
||||
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
|
||||
.setInterval(5000)
|
||||
LocationServices.FusedLocationApi.requestLocationUpdates(locationClient, request, this)
|
||||
requestingLocationUpdates = true
|
||||
}
|
||||
|
||||
private fun removeLocationUpdates() {
|
||||
LocationServices.FusedLocationApi.removeLocationUpdates(locationClient, this)
|
||||
}
|
||||
|
||||
override fun onConnectionSuspended() {
|
||||
}
|
||||
|
||||
override fun onLocationChanged(location: Location?) {
|
||||
val map = this.map ?: return
|
||||
if (location == null || vm.myLocationEnabled.value == false) return
|
||||
|
||||
val latLng = LatLng(location.latitude, location.longitude)
|
||||
vm.location.value = latLng
|
||||
val camUpdate = map.cameraUpdateFactory.newLatLng(latLng)
|
||||
map.animateCamera(camUpdate)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
removeLocationUpdates()
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package net.vonforst.evmap.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.car2go.maps.AnyMap
|
||||
import net.vonforst.evmap.R
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_CUSTOM
|
||||
import net.vonforst.evmap.viewmodel.FILTERS_DISABLED
|
||||
@@ -73,6 +74,12 @@ class PreferenceDataSource(val context: Context) {
|
||||
context.getString(R.string.pref_map_provider_default)
|
||||
)!!
|
||||
|
||||
var mapType: AnyMap.Type
|
||||
get() = AnyMap.Type.valueOf(sp.getString("map_type", null) ?: AnyMap.Type.NORMAL.toString())
|
||||
set(type) {
|
||||
sp.edit().putString("map_type", type.toString()).apply()
|
||||
}
|
||||
|
||||
var welcomeDialogShown: Boolean
|
||||
get() = sp.getBoolean("welcome_dialog_shown", false)
|
||||
set(value) {
|
||||
|
||||
@@ -163,8 +163,8 @@ fun setLinkify(textView: TextView, oldValue: Int, newValue: Int) {
|
||||
textView.linksClickable = newValue != 0
|
||||
|
||||
// remove spans
|
||||
if (newValue == 0) {
|
||||
val text = textView.text as SpannableString
|
||||
val text = textView.text
|
||||
if (newValue == 0 && text != null && text is SpannableString) {
|
||||
text.getSpans(0, text.length, Any::class.java).forEach {
|
||||
text.removeSpan(it)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,12 @@ class FilterProfilesViewModel(application: Application) : AndroidViewModel(appli
|
||||
}
|
||||
}
|
||||
|
||||
fun update(item: FilterProfile) {
|
||||
viewModelScope.launch {
|
||||
db.filterProfileDao().update(item)
|
||||
}
|
||||
}
|
||||
|
||||
fun reorderProfiles(list: List<FilterProfile>) {
|
||||
viewModelScope.launch {
|
||||
db.filterProfileDao().update(*list.toTypedArray())
|
||||
|
||||
@@ -135,8 +135,8 @@ private fun MediatorLiveData<List<Filter<FilterValue>>>.buildFilters(
|
||||
internal fun filtersWithValue(
|
||||
filters: LiveData<List<Filter<FilterValue>>>,
|
||||
filterValues: LiveData<List<FilterValue>>
|
||||
): MediatorLiveData<List<FilterWithValue<out FilterValue>>> =
|
||||
MediatorLiveData<List<FilterWithValue<out FilterValue>>>().apply {
|
||||
): MediatorLiveData<FilterValues> =
|
||||
MediatorLiveData<FilterValues>().apply {
|
||||
listOf(filters, filterValues).forEach {
|
||||
addSource(it) {
|
||||
val f = filters.value ?: return@addSource
|
||||
@@ -173,7 +173,7 @@ class FilterViewModel(application: Application, geApiKey: String) :
|
||||
db.filterValueDao().getFilterValues(FILTERS_CUSTOM)
|
||||
}
|
||||
|
||||
val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
|
||||
val filtersWithValue: LiveData<FilterValues> by lazy {
|
||||
filtersWithValue(filters, filterValues)
|
||||
}
|
||||
|
||||
@@ -331,5 +331,19 @@ data class SliderFilterValue(
|
||||
|
||||
data class FilterWithValue<T : FilterValue>(val filter: Filter<T>, val value: T) : Equatable
|
||||
|
||||
typealias FilterValues = List<FilterWithValue<out FilterValue>>
|
||||
|
||||
fun FilterValues.getBooleanValue(key: String) =
|
||||
(this.find { it.value.key == key }!!.value as BooleanFilterValue).value
|
||||
|
||||
fun FilterValues.getSliderValue(key: String) =
|
||||
(this.find { it.value.key == key }!!.value as SliderFilterValue).value
|
||||
|
||||
fun FilterValues.getMultipleChoiceFilter(key: String) =
|
||||
this.find { it.value.key == key }!!.filter as MultipleChoiceFilter
|
||||
|
||||
fun FilterValues.getMultipleChoiceValue(key: String) =
|
||||
this.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
|
||||
|
||||
const val FILTERS_DISABLED = -2L
|
||||
const val FILTERS_CUSTOM = -1L
|
||||
@@ -6,7 +6,6 @@ import com.car2go.maps.AnyMap
|
||||
import com.car2go.maps.model.LatLng
|
||||
import com.car2go.maps.model.LatLngBounds
|
||||
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
|
||||
@@ -37,7 +36,6 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
private var api = GoingElectricApi.create(geApiKey, context = application)
|
||||
private var db = AppDatabase.getInstance(application)
|
||||
private var prefs = PreferenceDataSource(application)
|
||||
private var chargepointLoader: Job? = null
|
||||
|
||||
val bottomSheetState: MutableLiveData<Int> by lazy {
|
||||
MutableLiveData<Int>()
|
||||
@@ -69,7 +67,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
private val filters = getFilters(application, plugs, networks, chargeCards)
|
||||
|
||||
private val filtersWithValue: LiveData<List<FilterWithValue<out FilterValue>>> by lazy {
|
||||
private val filtersWithValue: LiveData<FilterValues> by lazy {
|
||||
filtersWithValue(filters, filterValues)
|
||||
}
|
||||
|
||||
@@ -147,7 +145,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
val callback = { _: Any? ->
|
||||
val loc = location.value
|
||||
val charger = chargerSparse.value
|
||||
value = if (loc != null && charger != null && myLocationEnabled.value == true) {
|
||||
value = if (loc != null && charger != null) {
|
||||
distanceBetween(
|
||||
loc.latitude,
|
||||
loc.longitude,
|
||||
@@ -158,7 +156,6 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
addSource(chargerSparse, callback)
|
||||
addSource(location, callback)
|
||||
addSource(myLocationEnabled, callback)
|
||||
}
|
||||
}
|
||||
val location: MutableLiveData<LatLng> by lazy {
|
||||
@@ -177,6 +174,21 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
}
|
||||
}
|
||||
val filteredAvailability: MediatorLiveData<Resource<ChargeLocationStatus>> by lazy {
|
||||
MediatorLiveData<Resource<ChargeLocationStatus>>().apply {
|
||||
val callback = { _: Any? ->
|
||||
val av = availability.value
|
||||
val filters = filtersWithValue.value
|
||||
if (av?.status == Status.SUCCESS && filters != null) {
|
||||
value = Resource.success(av.data!!.applyFilters(filters))
|
||||
} else {
|
||||
value = av
|
||||
}
|
||||
}
|
||||
addSource(availability, callback)
|
||||
addSource(filtersWithValue, callback)
|
||||
}
|
||||
}
|
||||
val myLocationEnabled: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
@@ -196,7 +208,10 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
|
||||
val mapType: MutableLiveData<AnyMap.Type> by lazy {
|
||||
MutableLiveData<AnyMap.Type>().apply {
|
||||
value = AnyMap.Type.NORMAL
|
||||
value = prefs.mapType
|
||||
observeForever {
|
||||
prefs.mapType = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,39 +275,41 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
loadChargepoints(pos, filters)
|
||||
}
|
||||
|
||||
private fun loadChargepoints(
|
||||
mapPosition: MapPosition,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
) {
|
||||
chargepointLoader?.cancel()
|
||||
private var chargepointLoader =
|
||||
throttleLatest(500L, viewModelScope) { data: Pair<MapPosition, FilterValues> ->
|
||||
chargepoints.value = Resource.loading(chargepoints.value?.data)
|
||||
filteredConnectors.value = null
|
||||
filteredChargeCards.value = null
|
||||
|
||||
chargepoints.value = Resource.loading(chargepoints.value?.data)
|
||||
filteredConnectors.value = null
|
||||
filteredChargeCards.value = null
|
||||
val bounds = mapPosition.bounds
|
||||
val zoom = mapPosition.zoom
|
||||
chargepointLoader = viewModelScope.launch {
|
||||
val result = getChargepointsWithFilters(bounds, zoom, filters)
|
||||
val mapPosition = data.first
|
||||
val filters = data.second
|
||||
val result = getChargepointsWithFilters(mapPosition.bounds, mapPosition.zoom, filters)
|
||||
filteredConnectors.value = result.second
|
||||
filteredChargeCards.value = result.third
|
||||
chargepoints.value = result.first
|
||||
}
|
||||
|
||||
private fun loadChargepoints(
|
||||
mapPosition: MapPosition,
|
||||
filters: FilterValues
|
||||
) {
|
||||
chargepointLoader(Pair(mapPosition, filters))
|
||||
}
|
||||
|
||||
private suspend fun getChargepointsWithFilters(
|
||||
bounds: LatLngBounds,
|
||||
zoom: Float,
|
||||
filters: List<FilterWithValue<out FilterValue>>
|
||||
filters: FilterValues
|
||||
): Triple<Resource<List<ChargepointListItem>>, Set<String>?, Set<Long>?> {
|
||||
val freecharging = getBooleanValue(filters, "freecharging")
|
||||
val freeparking = getBooleanValue(filters, "freeparking")
|
||||
val open247 = getBooleanValue(filters, "open_247")
|
||||
val barrierfree = getBooleanValue(filters, "barrierfree")
|
||||
val excludeFaults = getBooleanValue(filters, "exclude_faults")
|
||||
val minPower = getSliderValue(filters, "min_power")
|
||||
val minConnectors = getSliderValue(filters, "min_connectors")
|
||||
val freecharging = filters.getBooleanValue("freecharging")
|
||||
val freeparking = filters.getBooleanValue("freeparking")
|
||||
val open247 = filters.getBooleanValue("open_247")
|
||||
val barrierfree = filters.getBooleanValue("barrierfree")
|
||||
val excludeFaults = filters.getBooleanValue("exclude_faults")
|
||||
val minPower = filters.getSliderValue("min_power")
|
||||
val minConnectors = filters.getSliderValue("min_connectors")
|
||||
|
||||
val connectorsVal = getMultipleChoiceValue(filters, "connectors")
|
||||
val connectorsVal = filters.getMultipleChoiceValue("connectors")
|
||||
if (connectorsVal.values.isEmpty() && !connectorsVal.all) {
|
||||
// no connectors chosen
|
||||
return Triple(Resource.success(emptyList()), null, null)
|
||||
@@ -300,7 +317,7 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
val connectors = formatMultipleChoice(connectorsVal)
|
||||
val filteredConnectors = if (connectorsVal.all) null else connectorsVal.values
|
||||
|
||||
val chargeCardsVal = getMultipleChoiceValue(filters, "chargecards")
|
||||
val chargeCardsVal = filters.getMultipleChoiceValue("chargecards")
|
||||
if (chargeCardsVal.values.isEmpty() && !chargeCardsVal.all) {
|
||||
// no chargeCards chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, null)
|
||||
@@ -309,14 +326,14 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
val filteredChargeCards =
|
||||
if (chargeCardsVal.all) null else chargeCardsVal.values.map { it.toLong() }.toSet()
|
||||
|
||||
val networksVal = getMultipleChoiceValue(filters, "networks")
|
||||
val networksVal = filters.getMultipleChoiceValue("networks")
|
||||
if (networksVal.values.isEmpty() && !networksVal.all) {
|
||||
// no networks chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
|
||||
}
|
||||
val networks = formatMultipleChoice(networksVal)
|
||||
|
||||
val categoriesVal = getMultipleChoiceValue(filters, "categories")
|
||||
val categoriesVal = filters.getMultipleChoiceValue("categories")
|
||||
if (categoriesVal.values.isEmpty() && !categoriesVal.all) {
|
||||
// no categories chosen
|
||||
return Triple(Resource.success(emptyList()), filteredConnectors, filteredChargeCards)
|
||||
@@ -398,26 +415,6 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
private fun formatMultipleChoice(connectorsVal: MultipleChoiceFilterValue) =
|
||||
if (connectorsVal.all) null else connectorsVal.values.joinToString(",")
|
||||
|
||||
private fun getBooleanValue(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = (filters.find { it.value.key == key }!!.value as BooleanFilterValue).value
|
||||
|
||||
private fun getSliderValue(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = (filters.find { it.value.key == key }!!.value as SliderFilterValue).value
|
||||
|
||||
private fun getMultipleChoiceFilter(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = filters.find { it.value.key == key }!!.filter as MultipleChoiceFilter
|
||||
|
||||
private fun getMultipleChoiceValue(
|
||||
filters: List<FilterWithValue<out FilterValue>>,
|
||||
key: String
|
||||
) = filters.find { it.value.key == key }!!.value as MultipleChoiceFilterValue
|
||||
|
||||
private suspend fun loadAvailability(charger: ChargeLocation) {
|
||||
availability.value = Resource.loading(null)
|
||||
availability.value = getAvailability(charger)
|
||||
@@ -445,4 +442,32 @@ class MapViewModel(application: Application, geApiKey: String) : AndroidViewMode
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun loadChargerById(chargerId: Long) {
|
||||
chargerDetails.value = Resource.loading(null)
|
||||
chargerSparse.value = null
|
||||
api.getChargepointDetail(chargerId).enqueue(object :
|
||||
Callback<ChargepointList> {
|
||||
override fun onFailure(call: Call<ChargepointList>, t: Throwable) {
|
||||
chargerSparse.value = null
|
||||
chargerDetails.value = Resource.error(t.message, null)
|
||||
t.printStackTrace()
|
||||
}
|
||||
|
||||
override fun onResponse(
|
||||
call: Call<ChargepointList>,
|
||||
response: Response<ChargepointList>
|
||||
) {
|
||||
if (!response.isSuccessful || response.body()!!.status != "ok") {
|
||||
chargerDetails.value = Resource.error(response.message(), null)
|
||||
chargerSparse.value = null
|
||||
} else {
|
||||
val charger = response.body()!!.chargelocations[0] as ChargeLocation
|
||||
chargerDetails.value =
|
||||
Resource.success(charger)
|
||||
chargerSparse.value = charger
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,10 @@ package net.vonforst.evmap.viewmodel
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.annotation.Nullable
|
||||
import androidx.lifecycle.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
||||
@@ -63,4 +67,27 @@ class SingleLiveEvent<T> : MutableLiveData<T>() {
|
||||
fun call() {
|
||||
value = null
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> throttleLatest(
|
||||
skipMs: Long = 300L,
|
||||
coroutineScope: CoroutineScope,
|
||||
destinationFunction: suspend (T) -> Unit
|
||||
): (T) -> Unit {
|
||||
var throttleJob: Job? = null
|
||||
var waitingParam: T? = null
|
||||
return { param: T ->
|
||||
if (throttleJob?.isCompleted != false) {
|
||||
throttleJob = coroutineScope.launch {
|
||||
destinationFunction(param)
|
||||
delay(skipMs)
|
||||
waitingParam?.let { wParam ->
|
||||
waitingParam = null
|
||||
destinationFunction(wParam)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
waitingParam = param
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,10 @@
|
||||
name="availability"
|
||||
type="Resource<ChargeLocationStatus>" />
|
||||
|
||||
<variable
|
||||
name="filteredAvailability"
|
||||
type="Resource<ChargeLocationStatus>" />
|
||||
|
||||
<variable
|
||||
name="chargeCards"
|
||||
type="java.util.Map<Long, ChargeCard>" />
|
||||
@@ -116,7 +120,7 @@
|
||||
android:gravity="end"
|
||||
android:maxLines="1"
|
||||
android:padding="2dp"
|
||||
android:text="@{String.format("%s/%d", BindingAdaptersKt.availabilityText(BindingAdaptersKt.flatten(availability.data.status.values())), charger.data.totalChargepoints)}"
|
||||
android:text="@{String.format("%s/%d", BindingAdaptersKt.availabilityText(BindingAdaptersKt.flatten(filteredAvailability.data.status.values())), filteredAvailability.data.totalChargepoints)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:textColor="@android:color/white"
|
||||
app:backgroundTintAvailability="@{BindingAdaptersKt.flatten(availability.data.status.values())}"
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
layout="@layout/detail_view"
|
||||
app:charger="@{vm.charger}"
|
||||
app:availability="@{vm.availability}"
|
||||
app:filteredAvailability="@{vm.filteredAvailability}"
|
||||
app:chargeCards="@{vm.chargeCardMap}"
|
||||
app:filteredChargeCards="@{vm.filteredChargeCards}"
|
||||
app:distance="@{vm.chargerDistance}"
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<string name="settings_ui">Oberfläche</string>
|
||||
<string name="settings_map">Karte</string>
|
||||
<string name="copyright">Copyright</string>
|
||||
<string name="copyright_summary">©2020 Johan von Forstner</string>
|
||||
<string name="copyright_summary">©2020–2021 Johan von Forstner</string>
|
||||
<string name="other">Sonstiges</string>
|
||||
<string name="privacy">Datenschutzerklärung</string>
|
||||
<string name="fav_add">Zu Favoriten hinzufügen</string>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<string name="settings_ui">User Interface</string>
|
||||
<string name="settings_map">Map</string>
|
||||
<string name="copyright">Copyright</string>
|
||||
<string name="copyright_summary">©2020 Johan von Forstner</string>
|
||||
<string name="copyright_summary">©2020–2021 Johan von Forstner</string>
|
||||
<string name="other">Other</string>
|
||||
<string name="privacy">Privacy Notice</string>
|
||||
<string name="fav_add">Add to favorites</string>
|
||||
|
||||
2
fastlane/metadata/android/de-DE/changelogs/36.txt
Normal file
2
fastlane/metadata/android/de-DE/changelogs/36.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Verbesserungen:
|
||||
- Verschiedene Abstürze behoben
|
||||
10
fastlane/metadata/android/de-DE/changelogs/39.txt
Normal file
10
fastlane/metadata/android/de-DE/changelogs/39.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
Neue Features:
|
||||
- Kartenausschnitt folgt der aktuellen Position
|
||||
- Verfügbarkeitsanzeige in der Kartenansicht beinhaltet nur die per Filter gewählten Anschlüsse (z.B. nur CCS)
|
||||
- Links zu https://www.goingelectric.de/stromtankstellen können in EVMap geöffnet werden
|
||||
- Geteilte Standorte (z.B. aus Messenger-Apps) können in EVMap geöffnet werden
|
||||
|
||||
Fehlerkorrekturen:
|
||||
- Filtereinstellungen wurden bei Umbenennen eines Filterprofils fälschlicherweise gelöscht
|
||||
- Ausgewählter Kartentyp (Satellit, Gelände, Standard) bleibt beim App-Neustart erhalten
|
||||
- Copyright-Jahr aktualisiert
|
||||
2
fastlane/metadata/android/en-US/changelogs/36.txt
Normal file
2
fastlane/metadata/android/en-US/changelogs/36.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
Improvements:
|
||||
- Fixed various crashes
|
||||
10
fastlane/metadata/android/en-US/changelogs/39.txt
Normal file
10
fastlane/metadata/android/en-US/changelogs/39.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
New Features:
|
||||
- Map follows current location
|
||||
- Availability indicator in map view only shows currently filtered connectors (e.g. only CCS)
|
||||
- Links to https://www.goingelectric.de/stromtankstellen can be opened in EVMap
|
||||
- Shared locations (e.g. from messenger apps) can be opened in EVMap
|
||||
|
||||
Bugfixes:
|
||||
- Filter settings would be deleted when renaming a saved filter profile
|
||||
- Selected map type (Default, Satellite, Terrain) will be kept across app restarts
|
||||
- Updated copyright year
|
||||
Reference in New Issue
Block a user