diff --git a/app/build.gradle b/app/build.gradle index b3acd096..df4b8ca3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' +apply plugin: 'androidx.navigation.safeargs.kotlin' android { compileSdkVersion 29 @@ -58,6 +59,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.core:core-ktx:1.2.0' 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.appcompat:appcompat:1.1.0' implementation 'com.google.android.material:material:1.1.0' @@ -73,6 +75,16 @@ dependencies { implementation 'com.squareup.picasso:picasso:2.71828' implementation 'com.github.MikeOrtiz:TouchImageView:2.3.3' + // navigation library + def nav_version = "2.3.0-alpha04" + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + + // viewmodel library + def lifecycle_version = "2.2.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + // debug tools implementation 'com.facebook.stetho:stetho:1.5.1' implementation 'com.facebook.stetho:stetho-okhttp3:1.5.1' diff --git a/app/src/main/java/com/johan/evmap/GalleryActivity.kt b/app/src/main/java/com/johan/evmap/GalleryActivity.kt index 80ba6d1d..c86c3d9e 100644 --- a/app/src/main/java/com/johan/evmap/GalleryActivity.kt +++ b/app/src/main/java/com/johan/evmap/GalleryActivity.kt @@ -12,6 +12,7 @@ import androidx.viewpager2.widget.ViewPager2 import com.johan.evmap.adapter.GalleryAdapter import com.johan.evmap.adapter.galleryTransitionName import com.johan.evmap.databinding.ActivityGalleryBinding +import com.johan.evmap.fragment.MapFragment import com.ortiz.touchview.TouchImageView @@ -68,8 +69,8 @@ class GalleryActivity : AppCompatActivity() { override fun finishAfterTransition() { isReturning = true val data = Intent() - data.putExtra(MapsActivity.EXTRA_STARTING_GALLERY_POSITION, startingPosition) - data.putExtra(MapsActivity.EXTRA_CURRENT_GALLERY_POSITION, currentPosition) + data.putExtra(MapFragment.EXTRA_STARTING_GALLERY_POSITION, startingPosition) + data.putExtra(MapFragment.EXTRA_CURRENT_GALLERY_POSITION, currentPosition) setResult(Activity.RESULT_OK, data) super.finishAfterTransition() } diff --git a/app/src/main/java/com/johan/evmap/MapsActivity.kt b/app/src/main/java/com/johan/evmap/MapsActivity.kt index f3496518..dc7935b1 100644 --- a/app/src/main/java/com/johan/evmap/MapsActivity.kt +++ b/app/src/main/java/com/johan/evmap/MapsActivity.kt @@ -1,424 +1,70 @@ package com.johan.evmap -import android.Manifest -import android.annotation.SuppressLint import android.content.Intent -import android.content.pm.PackageManager -import android.content.res.Configuration import android.net.Uri import android.os.Bundle -import android.view.View -import android.view.ViewTreeObserver -import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.app.ActivityOptionsCompat -import androidx.core.app.SharedElementCallback -import androidx.core.content.ContextCompat -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.ViewModel -import androidx.lifecycle.lifecycleScope -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.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.material.snackbar.Snackbar -import com.johan.evmap.adapter.ConnectorAdapter -import com.johan.evmap.adapter.DetailAdapter -import com.johan.evmap.adapter.GalleryAdapter -import com.johan.evmap.adapter.galleryTransitionName -import com.johan.evmap.api.* -import com.johan.evmap.databinding.ActivityMapsBinding -import com.johan.evmap.ui.ClusterIconGenerator -import com.johan.evmap.ui.getBitmapDescriptor -import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response -import java.io.IOException +import androidx.navigation.NavController +import androidx.navigation.findNavController +import com.johan.evmap.api.ChargeLocation const val REQUEST_LOCATION_PERMISSION = 1 -class MapsActivityViewModel : ViewModel() { - val chargepoints: MutableLiveData> by lazy { - MutableLiveData>().apply { - value = emptyList() - } - } - val charger: MutableLiveData by lazy { - MutableLiveData() - } - val availability: MutableLiveData by lazy { - MutableLiveData() - } - val myLocationEnabled: MutableLiveData by lazy { - MutableLiveData() - } -} -class MapsActivity : AppCompatActivity(), OnMapReadyCallback { - private lateinit var binding: ActivityMapsBinding - private var map: GoogleMap? = null - private lateinit var api: GoingElectricApi - private lateinit var fusedLocationClient: FusedLocationProviderClient - private val vm: MapsActivityViewModel by viewModels() - private var markers: Map = emptyMap() - private var clusterMarkers: List = emptyList() - private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike - - private var reenterState: Bundle? = null +class MapsActivity : AppCompatActivity() { + private lateinit var navController: NavController override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - fusedLocationClient = LocationServices.getFusedLocationProviderClient(this) - api = GoingElectricApi.create(getString(R.string.goingelectric_key)) - binding = DataBindingUtil.setContentView(this, R.layout.activity_maps) - binding.lifecycleOwner = this - binding.vm = vm + setContentView(R.layout.activity_maps) - ActivityCompat.setExitSharedElementCallback(this, exitElementCallback) - setSupportActionBar(binding.toolbar) - title = "" + //ActivityCompat.setExitSharedElementCallback(this, exitElementCallback) + //setSupportActionBar(binding.toolbar) + //title = "" - val mapFragment = supportFragmentManager.findFragmentById(R.id.map) as SupportMapFragment - mapFragment.getMapAsync(this) - - setupAdapters() - - bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet) - - vm.charger.observe(this, object : Observer { - var previousCharger = vm.charger.value - - override fun onChanged(charger: ChargeLocation?) { - if (charger != null) { - if (previousCharger == null || - previousCharger!!.id != charger.id - ) { - vm.availability.value = null - bottomSheetBehavior.state = - BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED - loadChargerDetails() - } - } else { - bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN - } - previousCharger = charger - } - }) - vm.chargepoints.observe(this, Observer { - updateMap(it) - }) - - binding.fabLocate.setOnClickListener { - if (!hasLocationPermission()) { - ActivityCompat.requestPermissions( - this, - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), - REQUEST_LOCATION_PERMISSION - ) - } else { - enableLocation(true) - } - } - binding.fabDirections.setOnClickListener { - val charger = vm.charger.value - if (charger != null) { - val intent = Intent(Intent.ACTION_VIEW) - val coord = charger.coordinates - - // google maps navigation - intent.data = Uri.parse("google.navigation:q=${coord.lat},${coord.lng}") - if (intent.resolveActivity(packageManager) != null) { - startActivity(intent); - } else { - // fallback: generic geo intent - intent.data = Uri.parse("geo:${coord.lat},${coord.lng}") - if (intent.resolveActivity(packageManager) != null) { - startActivity(intent); - } else { - Snackbar.make( - binding.root, - R.string.no_maps_app_found, - Snackbar.LENGTH_SHORT - ) - } - } - } - } - binding.detailView.goingelectricButton.setOnClickListener { - val charger = vm.charger.value - if (charger != null) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https:${charger.url}")) - startActivity(intent) - } - } - binding.detailView.topPart.setOnClickListener { - bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT - } - } - - private fun setupAdapters() { - val galleryClickListener = object : GalleryAdapter.ItemClickListener { - override fun onItemClick(view: View, position: Int) { - val photos = vm.charger.value?.photos ?: return - val intent = Intent(this@MapsActivity, GalleryActivity::class.java).apply { - putExtra(GalleryActivity.EXTRA_PHOTOS, ArrayList(photos)) - putExtra(GalleryActivity.EXTRA_POSITION, position) - } - val options = ActivityOptionsCompat.makeSceneTransitionAnimation( - this@MapsActivity, view, view.transitionName - ) - startActivity(intent, options.toBundle()) - } - } - - binding.gallery.apply { - adapter = GalleryAdapter(this@MapsActivity, galleryClickListener) - layoutManager = - LinearLayoutManager(this@MapsActivity, LinearLayoutManager.HORIZONTAL, false) - addItemDecoration(DividerItemDecoration( - this@MapsActivity, LinearLayoutManager.HORIZONTAL - ).apply { - setDrawable(getDrawable(R.drawable.gallery_divider)!!) - }) - } - - binding.detailView.connectors.apply { - adapter = ConnectorAdapter() - layoutManager = - LinearLayoutManager(this@MapsActivity, LinearLayoutManager.HORIZONTAL, false) - } - - binding.detailView.details.apply { - adapter = DetailAdapter() - layoutManager = - LinearLayoutManager(this@MapsActivity, LinearLayoutManager.VERTICAL, false) - addItemDecoration( - DividerItemDecoration( - this@MapsActivity, - LinearLayoutManager.VERTICAL - ) - ) - } - } - - - override fun onMapReady(map: GoogleMap) { - this.map = map - map.setOnCameraIdleListener { - loadChargepoints() - } - map.setOnMarkerClickListener { marker -> - when (marker) { - in markers -> { - vm.charger.value = markers[marker] - true - } - in clusterMarkers -> { - val newZoom = map.cameraPosition.zoom + 2 - map.animateCamera(CameraUpdateFactory.newLatLngZoom(marker.position, newZoom)) - true - } - else -> false - } - } - map.setOnMapClickListener { - vm.charger.value = null - } - - val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - map.setMapStyle( - if (mode == Configuration.UI_MODE_NIGHT_YES) { - MapStyleOptions.loadRawResourceStyle(this, R.raw.maps_night_mode) - } else null - ) - - - if (hasLocationPermission()) { - enableLocation(false) - } else { - // center the camera on Europe - val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f) - map.moveCamera(cameraUpdate) - } - } - - @SuppressLint("MissingPermission") - private fun enableLocation(animate: Boolean) { - val map = this.map ?: return - map.isMyLocationEnabled = true - vm.myLocationEnabled.value = true - map.uiSettings.isMyLocationButtonEnabled = false - fusedLocationClient.lastLocation.addOnSuccessListener { location -> - if (location != null) { - val latLng = LatLng(location.latitude, location.longitude) - val camUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 13f) - if (animate) { - map.animateCamera(camUpdate) - } else { - map.moveCamera(camUpdate) - } - } - } - } - - private fun hasLocationPermission() = - ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == - PackageManager.PERMISSION_GRANTED - - private fun loadChargepoints() { - val map = this.map ?: return - val bounds = map.projection.visibleRegion.latLngBounds - api.getChargepoints( - bounds.southwest.latitude, bounds.southwest.longitude, - bounds.northeast.latitude, bounds.northeast.longitude, - clustering = map.cameraPosition.zoom < 12, zoom = map.cameraPosition.zoom, - clusterDistance = 70 - ).enqueue(object : Callback { - override fun onFailure(call: Call, t: Throwable) { - //TODO: show error message - t.printStackTrace() - } - - override fun onResponse( - call: Call, - response: Response - ) { - if (!response.isSuccessful || response.body()!!.status != "ok") { - //TODO: show error message - return - } - - vm.chargepoints.value = response.body()!!.chargelocations - } - }) - } - - private fun loadChargerDetails() { - val charger = vm.charger.value ?: return - api.getChargepointDetail(charger.id).enqueue(object : Callback { - override fun onFailure(call: Call, t: Throwable) { - //TODO: show error message - t.printStackTrace() - } - - override fun onResponse( - call: Call, - response: Response - ) { - if (!response.isSuccessful || response.body()!!.status != "ok") { - //TODO: show error message - return - } - - vm.charger.value = response.body()!!.chargelocations[0] as ChargeLocation - } - }) - - lifecycleScope.launch { - loadChargerAvailability(charger) - } - } - - private suspend fun loadChargerAvailability(charger: ChargeLocation) { - var availability: ChargeLocationStatus? = null - withContext(Dispatchers.IO) { - for (ad in availabilityDetectors) { - try { - availability = ad.getAvailability(charger) - break - } catch (e: IOException) { - e.printStackTrace() - } catch (e: AvailabilityDetectorException) { - e.printStackTrace() - } - } - } - vm.availability.value = availability - } - - private fun updateMap(chargepoints: List) { - val map = this.map ?: return - markers.keys.forEach { it.remove() } - clusterMarkers.forEach { it.remove() } - - val iconGenerator = ClusterIconGenerator(this) - val clusters = chargepoints.filterIsInstance() - val chargers = chargepoints.filterIsInstance() - - markers = chargers.map { charger -> - map.addMarker( - MarkerOptions() - .position(LatLng(charger.coordinates.lat, charger.coordinates.lng)) - .icon( - getBitmapDescriptor( - R.drawable.ic_map_marker_charging, when { - charger.maxPower >= 100 -> R.color.charger_100kw - charger.maxPower >= 43 -> R.color.charger_43kw - charger.maxPower >= 20 -> R.color.charger_20kw - charger.maxPower >= 11 -> R.color.charger_11kw - else -> R.color.charger_low - }, this - ) - ) - ) to charger - }.toMap() - clusterMarkers = clusters.map { cluster -> - map.addMarker( - MarkerOptions() - .position(LatLng(cluster.coordinates.lat, cluster.coordinates.lng)) - .icon(BitmapDescriptorFactory.fromBitmap(iconGenerator.makeIcon(cluster.clusterCount.toString()))) - ) - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - when (requestCode) { - REQUEST_LOCATION_PERMISSION -> { - if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { - enableLocation(true) - } - } - else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) - } + navController = findNavController(R.id.nav_host_fragment) } override fun onBackPressed() { - if (bottomSheetBehavior.state != BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED && - bottomSheetBehavior.state != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN - ) { - bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED - } else if (bottomSheetBehavior.state == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED) { - vm.charger.value = null + + } + + fun navigateTo(charger: ChargeLocation) { + val intent = Intent(Intent.ACTION_VIEW) + val coord = charger.coordinates + + // google maps navigation + intent.data = Uri.parse("google.navigation:q=${coord.lat},${coord.lng}") + val pm = packageManager + if (intent.resolveActivity(pm) != null) { + startActivity(intent); } else { - super.onBackPressed() + // fallback: generic geo intent + intent.data = Uri.parse("geo:${coord.lat},${coord.lng}") + if (intent.resolveActivity(pm) != null) { + startActivity(intent); + } else { + // TODO: + /* + Snackbar.make( + , + R.string.no_maps_app_found, + Snackbar.LENGTH_SHORT + )*/ + } } } - private val exitElementCallback = object : SharedElementCallback() { + + /*private val exitElementCallback = object : SharedElementCallback() { override fun onMapSharedElements( names: MutableList, sharedElements: MutableMap ) { if (reenterState != null) { - val startingPosition = reenterState!!.getInt(EXTRA_STARTING_GALLERY_POSITION) - val currentPosition = reenterState!!.getInt(EXTRA_CURRENT_GALLERY_POSITION) + val startingPosition = reenterState!!.getInt(MapFragment.EXTRA_STARTING_GALLERY_POSITION) + val currentPosition = reenterState!!.getInt(MapFragment.EXTRA_CURRENT_GALLERY_POSITION) if (startingPosition != currentPosition) { // Current element has changed, need to override previous exit transitions val newTransitionName = galleryTransitionName(currentPosition) @@ -435,15 +81,15 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback { reenterState = null } } - } + }*/ - override fun onActivityReenter(resultCode: Int, data: Intent) { + /*override fun onActivityReenter(resultCode: Int, data: Intent) { // returning to gallery super.onActivityReenter(resultCode, data) reenterState = Bundle(data.extras) reenterState?.let { - val startingPosition = it.getInt(EXTRA_STARTING_GALLERY_POSITION) - val currentPosition = it.getInt(EXTRA_CURRENT_GALLERY_POSITION) + val startingPosition = it.getInt(MapFragment.EXTRA_STARTING_GALLERY_POSITION) + val currentPosition = it.getInt(MapFragment.EXTRA_CURRENT_GALLERY_POSITION) if (startingPosition != currentPosition) binding.gallery.scrollToPosition( currentPosition ) @@ -458,10 +104,5 @@ class MapsActivity : AppCompatActivity(), OnMapReadyCallback { } }) } - } - - companion object { - const val EXTRA_STARTING_GALLERY_POSITION = "extra_starting_item_position" - const val EXTRA_CURRENT_GALLERY_POSITION = "extra_current_item_position" - } + }*/ } diff --git a/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt b/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt new file mode 100644 index 00000000..d6463a98 --- /dev/null +++ b/app/src/main/java/com/johan/evmap/fragment/MapFragment.kt @@ -0,0 +1,422 @@ +package com.johan.evmap.fragment + +import android.Manifest +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.net.Uri +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.* +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.AppBarConfiguration +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.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.johan.evmap.GalleryActivity +import com.johan.evmap.MapsActivity +import com.johan.evmap.R +import com.johan.evmap.REQUEST_LOCATION_PERMISSION +import com.johan.evmap.adapter.ConnectorAdapter +import com.johan.evmap.adapter.DetailAdapter +import com.johan.evmap.adapter.GalleryAdapter +import com.johan.evmap.api.* +import com.johan.evmap.databinding.FragmentMapBinding +import com.johan.evmap.ui.ClusterIconGenerator +import com.johan.evmap.ui.getBitmapDescriptor +import com.mahc.custombottomsheetbehavior.BottomSheetBehaviorGoogleMapsLike +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.IOException + +class MapViewModel : ViewModel() { + val chargepoints: MutableLiveData> by lazy { + MutableLiveData>().apply { + value = emptyList() + } + } + val charger: MutableLiveData by lazy { + MutableLiveData() + } + val availability: MutableLiveData by lazy { + MutableLiveData() + } + val myLocationEnabled: MutableLiveData by lazy { + MutableLiveData() + } +} + +class MapFragment : Fragment(), OnMapReadyCallback { + private lateinit var binding: FragmentMapBinding + private val vm: MapViewModel by viewModels() + private var map: GoogleMap? = null + private lateinit var api: GoingElectricApi + private lateinit var fusedLocationClient: FusedLocationProviderClient + private lateinit var bottomSheetBehavior: BottomSheetBehaviorGoogleMapsLike + private var markers: Map = emptyMap() + private var clusterMarkers: List = emptyList() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = DataBindingUtil.inflate(inflater, R.layout.fragment_map, container, false) + binding.lifecycleOwner = this + binding.vm = vm + + fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireContext()) + api = GoingElectricApi.create(getString(R.string.goingelectric_key)) + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val mapFragment = childFragmentManager.findFragmentById(R.id.map) as SupportMapFragment + mapFragment.getMapAsync(this) + bottomSheetBehavior = BottomSheetBehaviorGoogleMapsLike.from(binding.bottomSheet) + + setupObservers() + setupClickListeners() + setupAdapters() + + val navController = findNavController() + val appBarConfiguration = AppBarConfiguration(navController.graph) + + view.findViewById(R.id.toolbar) + .setupWithNavController(navController, appBarConfiguration) + } + + private fun setupClickListeners() { + binding.fabLocate.setOnClickListener { + if (!hasLocationPermission()) { + requestPermissions( + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + REQUEST_LOCATION_PERMISSION + ) + } else { + enableLocation(true) + } + } + binding.fabDirections.setOnClickListener { + val charger = vm.charger.value + if (charger != null) { + if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + (requireActivity() as MapsActivity).navigateTo(charger) + } + } + } + binding.detailView.goingelectricButton.setOnClickListener { + val charger = vm.charger.value + if (charger != null) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https:${charger.url}")) + startActivity(intent) + } + } + binding.detailView.topPart.setOnClickListener { + bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_ANCHOR_POINT + } + } + + private fun setupObservers() { + vm.charger.observe(viewLifecycleOwner, object : Observer { + var previousCharger = vm.charger.value + + override fun onChanged(charger: ChargeLocation?) { + if (charger != null) { + if (previousCharger == null || + previousCharger!!.id != charger.id + ) { + vm.availability.value = null + bottomSheetBehavior.state = + BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED + loadChargerDetails() + } + } else { + bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN + } + previousCharger = charger + } + }) + vm.chargepoints.observe(viewLifecycleOwner, Observer { + updateMap(it) + }) + } + + private fun setupAdapters() { + val galleryClickListener = object : GalleryAdapter.ItemClickListener { + override fun onItemClick(view: View, position: Int) { + val photos = vm.charger.value?.photos ?: return + val intent = Intent(context, GalleryActivity::class.java).apply { + putExtra(GalleryActivity.EXTRA_PHOTOS, ArrayList(photos)) + putExtra(GalleryActivity.EXTRA_POSITION, position) + } + // TODO: + /*val options = ActivityOptionsCompat.makeSceneTransitionAnimation( + activity, view, view.transitionName + ) + startActivity(intent, options.toBundle())*/ + startActivity(intent) + } + } + + binding.gallery.apply { + adapter = GalleryAdapter(context, galleryClickListener) + layoutManager = + LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + addItemDecoration( + DividerItemDecoration( + context, LinearLayoutManager.HORIZONTAL + ).apply { + setDrawable(context.getDrawable(R.drawable.gallery_divider)!!) + }) + } + + binding.detailView.connectors.apply { + adapter = ConnectorAdapter() + layoutManager = + LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + } + + binding.detailView.details.apply { + adapter = DetailAdapter() + layoutManager = + LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) + addItemDecoration( + DividerItemDecoration( + context, + LinearLayoutManager.VERTICAL + ) + ) + } + } + + override fun onMapReady(map: GoogleMap) { + this.map = map + map.setOnCameraIdleListener { + loadChargepoints() + } + map.setOnMarkerClickListener { marker -> + when (marker) { + in markers -> { + vm.charger.value = markers[marker] + true + } + in clusterMarkers -> { + val newZoom = map.cameraPosition.zoom + 2 + map.animateCamera(CameraUpdateFactory.newLatLngZoom(marker.position, newZoom)) + true + } + else -> false + } + } + map.setOnMapClickListener { + vm.charger.value = null + } + + val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + map.setMapStyle( + if (mode == Configuration.UI_MODE_NIGHT_YES) { + MapStyleOptions.loadRawResourceStyle(context, R.raw.maps_night_mode) + } else null + ) + + + if (hasLocationPermission()) { + enableLocation(false) + } else { + // center the camera on Europe + val cameraUpdate = CameraUpdateFactory.newLatLngZoom(LatLng(50.113388, 9.252536), 3.5f) + map.moveCamera(cameraUpdate) + } + } + + @SuppressLint("MissingPermission") + private fun enableLocation(animate: Boolean) { + val map = this.map ?: return + map.isMyLocationEnabled = true + vm.myLocationEnabled.value = true + map.uiSettings.isMyLocationButtonEnabled = false + fusedLocationClient.lastLocation.addOnSuccessListener { location -> + if (location != null) { + val latLng = LatLng(location.latitude, location.longitude) + val camUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 13f) + if (animate) { + map.animateCamera(camUpdate) + } else { + map.moveCamera(camUpdate) + } + } + } + } + + private fun hasLocationPermission(): Boolean { + val context = context ?: return false + return ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION + ) == + PackageManager.PERMISSION_GRANTED + } + + private fun loadChargepoints() { + val map = this.map ?: return + val bounds = map.projection.visibleRegion.latLngBounds + api.getChargepoints( + bounds.southwest.latitude, bounds.southwest.longitude, + bounds.northeast.latitude, bounds.northeast.longitude, + clustering = map.cameraPosition.zoom < 12, zoom = map.cameraPosition.zoom, + clusterDistance = 70 + ).enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + //TODO: show error message + t.printStackTrace() + } + + override fun onResponse( + call: Call, + response: Response + ) { + if (!response.isSuccessful || response.body()!!.status != "ok") { + //TODO: show error message + return + } + + vm.chargepoints.value = response.body()!!.chargelocations + } + }) + } + + private fun loadChargerDetails() { + val charger = vm.charger.value ?: return + api.getChargepointDetail(charger.id).enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + //TODO: show error message + t.printStackTrace() + } + + override fun onResponse( + call: Call, + response: Response + ) { + if (!response.isSuccessful || response.body()!!.status != "ok") { + //TODO: show error message + return + } + + vm.charger.value = response.body()!!.chargelocations[0] as ChargeLocation + } + }) + + lifecycleScope.launch { + loadChargerAvailability(charger) + } + } + + private suspend fun loadChargerAvailability(charger: ChargeLocation) { + var availability: ChargeLocationStatus? = null + withContext(Dispatchers.IO) { + for (ad in availabilityDetectors) { + try { + availability = ad.getAvailability(charger) + break + } catch (e: IOException) { + e.printStackTrace() + } catch (e: AvailabilityDetectorException) { + e.printStackTrace() + } + } + } + vm.availability.value = availability + } + + private fun updateMap(chargepoints: List) { + val map = this.map ?: return + markers.keys.forEach { it.remove() } + clusterMarkers.forEach { it.remove() } + + val context = context ?: return + val iconGenerator = ClusterIconGenerator(context) + val clusters = chargepoints.filterIsInstance() + val chargers = chargepoints.filterIsInstance() + + markers = chargers.map { charger -> + map.addMarker( + MarkerOptions() + .position(LatLng(charger.coordinates.lat, charger.coordinates.lng)) + .icon( + getBitmapDescriptor( + R.drawable.ic_map_marker_charging, when { + charger.maxPower >= 100 -> R.color.charger_100kw + charger.maxPower >= 43 -> R.color.charger_43kw + charger.maxPower >= 20 -> R.color.charger_20kw + charger.maxPower >= 11 -> R.color.charger_11kw + else -> R.color.charger_low + }, context + ) + ) + ) to charger + }.toMap() + clusterMarkers = clusters.map { cluster -> + map.addMarker( + MarkerOptions() + .position(LatLng(cluster.coordinates.lat, cluster.coordinates.lng)) + .icon(BitmapDescriptorFactory.fromBitmap(iconGenerator.makeIcon(cluster.clusterCount.toString()))) + ) + } + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + when (requestCode) { + REQUEST_LOCATION_PERMISSION -> { + if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) { + enableLocation(true) + } + } + else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + + fun goBack(): Boolean { + if (bottomSheetBehavior.state != BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED && + bottomSheetBehavior.state != BottomSheetBehaviorGoogleMapsLike.STATE_HIDDEN + ) { + bottomSheetBehavior.state = BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED + return true + } else if (bottomSheetBehavior.state == BottomSheetBehaviorGoogleMapsLike.STATE_COLLAPSED) { + vm.charger.value = null + return true + } else { + return false + } + } + + + companion object { + const val EXTRA_STARTING_GALLERY_POSITION = "extra_starting_item_position" + const val EXTRA_CURRENT_GALLERY_POSITION = "extra_current_item_position" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_maps.xml b/app/src/main/res/layout/activity_maps.xml index 85226dcb..c7ceae27 100644 --- a/app/src/main/res/layout/activity_maps.xml +++ b/app/src/main/res/layout/activity_maps.xml @@ -1,111 +1,21 @@ - + android:layout_width="match_parent" + android:layout_height="match_parent"> - - - - - - - - + android:layout_width="match_parent" + app:defaultNavHost="true" + app:navGraph="@navigation/nav_graph" /> - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_map.xml b/app/src/main/res/layout/fragment_map.xml new file mode 100644 index 00000000..0dd2bca0 --- /dev/null +++ b/app/src/main/res/layout/fragment_map.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 00000000..e3aeb9ed --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0310d6bf..a5e175de 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -21,4 +21,5 @@ Echtzeitstatus nicht verfügbar Quelle Echtzeitdaten (beta): %s Quelle: goingelectric.de + Suche \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e312dbed..903685ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,4 +20,5 @@ Real-time status unavailable Real-time status source (beta): %s Source: goingelectric.de + Search diff --git a/build.gradle b/build.gradle index f798db9f..1c424d74 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,9 @@ buildscript { classpath 'com.android.tools.build:gradle:4.0.0-beta03' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + def nav_version = "2.3.0-alpha04" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }