From fbbb4f9f485f68eb9654be41bd27a5c9b3452bd6 Mon Sep 17 00:00:00 2001 From: Severiano Jaramillo Date: Wed, 23 Jan 2019 19:15:06 -0600 Subject: [PATCH] Add the map icons for teller and teller cluster. Create TellerRepository to delegate it the responsability to keep the tellers info up to date into the database and serve the info to the MerchantsViewModel and MerchantsFragment immediatly. Create TellerMarketRenderer which is in charge of creating and serving the icons used for the tellers and tellers' clusters in the map. --- .../database/daos/TellerDao.kt | 8 ++ .../database/entities/Teller.kt | 21 ++++- .../fragments/MerchantsFragment.kt | 68 +++++++++----- .../repositories/TellerRepository.kt | 75 ++++++++++++++++ .../utils/TellerMarkerRenderer.kt | 83 ++++++++++++++++++ .../viewmodels/MerchantViewModel.kt | 7 ++ .../main/res/drawable/ic_teller_cluster.png | Bin 0 -> 3276 bytes app/src/main/res/drawable/ic_teller_pin.xml | 15 ++++ 8 files changed, 250 insertions(+), 27 deletions(-) create mode 100644 app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/TellerRepository.kt create mode 100644 app/src/main/java/cy/agorise/bitsybitshareswallet/utils/TellerMarkerRenderer.kt create mode 100644 app/src/main/res/drawable/ic_teller_cluster.png create mode 100644 app/src/main/res/drawable/ic_teller_pin.xml diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/TellerDao.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/TellerDao.kt index 7009f07..64b5ec7 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/TellerDao.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/daos/TellerDao.kt @@ -1,8 +1,10 @@ package cy.agorise.bitsybitshareswallet.database.daos +import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy +import androidx.room.Query import cy.agorise.bitsybitshareswallet.database.entities.Teller @Dao @@ -12,4 +14,10 @@ interface TellerDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insertAll(tellers: List) + + @Query("SELECT * FROM tellers") + fun getAll(): LiveData> + + @Query("DELETE FROM tellers") + fun deleteAll() } \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/entities/Teller.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/entities/Teller.kt index 714f293..71adaee 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/database/entities/Teller.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/database/entities/Teller.kt @@ -3,6 +3,8 @@ package cy.agorise.bitsybitshareswallet.database.entities import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.clustering.ClusterItem @Entity(tableName = "tellers") data class Teller( @@ -10,9 +12,22 @@ data class Teller( @ColumnInfo(name = "id") val _id: String, @ColumnInfo(name = "name") val gt_name: String, @ColumnInfo(name = "address") val address: String?, - @ColumnInfo(name = "lat") val lat: Float, - @ColumnInfo(name = "lon") val lon: Float, + @ColumnInfo(name = "lat") val lat: Double, + @ColumnInfo(name = "lon") val lon: Double, @ColumnInfo(name = "phone") val phone: String?, @ColumnInfo(name = "telegram") val telegram: String?, @ColumnInfo(name = "website") val url: String? -) \ No newline at end of file +) : ClusterItem { + override fun getSnippet(): String { + return address ?: "" + } + + override fun getTitle(): String { + return gt_name + } + + override fun getPosition(): LatLng { + return LatLng(lat, lon) + } + +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/MerchantsFragment.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/MerchantsFragment.kt index f316302..998866e 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/MerchantsFragment.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/fragments/MerchantsFragment.kt @@ -23,8 +23,10 @@ import com.google.android.gms.maps.model.* import com.google.maps.android.clustering.Cluster import com.google.maps.android.clustering.ClusterManager import cy.agorise.bitsybitshareswallet.database.entities.Merchant +import cy.agorise.bitsybitshareswallet.database.entities.Teller import cy.agorise.bitsybitshareswallet.utils.Constants import cy.agorise.bitsybitshareswallet.utils.MerchantMarkerRenderer +import cy.agorise.bitsybitshareswallet.utils.TellerMarkerRenderer import cy.agorise.bitsybitshareswallet.utils.toast import cy.agorise.bitsybitshareswallet.viewmodels.MerchantViewModel import java.lang.Exception @@ -46,7 +48,8 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, private lateinit var mMerchantViewModel: MerchantViewModel - private var mClusterManager: ClusterManager? = null + private var mMerchantClusterManager: ClusterManager? = null + private var mTellerClusterManager: ClusterManager? = null override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -107,27 +110,9 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, verifyLocationPermission() - // Setup clusters to group markers when possible - mClusterManager = ClusterManager(context, mMap) - val renderer = MerchantMarkerRenderer(context, mMap, mClusterManager) - mClusterManager?.renderer = renderer + initMerchantsCluster() - // Point the map's listeners at the listeners implemented by the cluster manager. - mMap.setOnCameraIdleListener(mClusterManager) - mMap.setOnMarkerClickListener(mClusterManager) - - mMap.setOnMarkerClickListener(mClusterManager) - mMap.setInfoWindowAdapter(mClusterManager?.markerManager) - mMap.setOnInfoWindowClickListener(mClusterManager) - mClusterManager?.setOnClusterClickListener(this) - mClusterManager?.setOnClusterItemClickListener(this) - mClusterManager?.setOnClusterItemInfoWindowClickListener(this) - - mMerchantViewModel.getAllMerchants().observe(this, Observer> {merchants -> - mClusterManager?.clearItems() - mClusterManager?.addItems(merchants) - mClusterManager?.cluster() - }) + initTellersCluster() } private fun applyMapTheme() { @@ -149,9 +134,44 @@ class MerchantsFragment : Fragment(), OnMapReadyCallback, } } - /** - * Animates the camera update to focus on an area that shows all the items from the cluster that was tapped. - */ + private fun initMerchantsCluster() { + // Setup clusters to group markers when possible + mMerchantClusterManager = ClusterManager(context, mMap) + val merchantRenderer = MerchantMarkerRenderer(context, mMap, mMerchantClusterManager) + mMerchantClusterManager?.renderer = merchantRenderer + + // Point the map's listeners at the listeners implemented by the cluster manager. + mMap.setOnCameraIdleListener(mMerchantClusterManager) + mMap.setOnMarkerClickListener(mMerchantClusterManager) + + mMap.setOnMarkerClickListener(mMerchantClusterManager) + mMap.setInfoWindowAdapter(mMerchantClusterManager?.markerManager) + mMap.setOnInfoWindowClickListener(mMerchantClusterManager) + mMerchantClusterManager?.setOnClusterClickListener(this) + mMerchantClusterManager?.setOnClusterItemClickListener(this) + mMerchantClusterManager?.setOnClusterItemInfoWindowClickListener(this) + + mMerchantViewModel.getAllMerchants().observe(this, Observer> {merchants -> + mMerchantClusterManager?.clearItems() + mMerchantClusterManager?.addItems(merchants) + mMerchantClusterManager?.cluster() + }) + } + + private fun initTellersCluster() { + // Setup clusters to group markers when possible + mTellerClusterManager = ClusterManager(context, mMap) + val tellerRenderer = TellerMarkerRenderer(context, mMap, mTellerClusterManager) + mTellerClusterManager?.renderer = tellerRenderer + + mMerchantViewModel.getAllTellers().observe(this, Observer> {tellers -> + mTellerClusterManager?.clearItems() + mTellerClusterManager?.addItems(tellers) + mTellerClusterManager?.cluster() + }) + } + + /** Animates the camera update to focus on an area that shows all the items from the cluster that was tapped. */ override fun onClusterClick(cluster: Cluster?): Boolean { val builder = LatLngBounds.builder() val merchantMarkers = cluster?.items diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/TellerRepository.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/TellerRepository.kt new file mode 100644 index 0000000..21404ad --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/repositories/TellerRepository.kt @@ -0,0 +1,75 @@ +package cy.agorise.bitsybitshareswallet.repositories + +import android.content.Context +import android.os.AsyncTask +import android.preference.PreferenceManager +import androidx.lifecycle.LiveData +import cy.agorise.bitsybitshareswallet.database.BitsyDatabase +import cy.agorise.bitsybitshareswallet.database.daos.TellerDao +import cy.agorise.bitsybitshareswallet.database.entities.Teller +import cy.agorise.bitsybitshareswallet.network.FeathersResponse +import cy.agorise.bitsybitshareswallet.network.MerchantsWebservice +import cy.agorise.bitsybitshareswallet.utils.Constants +import retrofit2.Call +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory + +class TellerRepository internal constructor(val context: Context) : retrofit2.Callback> { + + private val mTellerDao: TellerDao + + init { + val db = BitsyDatabase.getDatabase(context) + mTellerDao = db!!.tellerDao() + } + + /** Returns a LiveData object directly from the database while the response from the WebService is obtained. */ + fun getAll(): LiveData> { + refreshTellers() + return mTellerDao.getAll() + } + + /** Refreshes the tellers information only if the MERCHANT_UPDATE_PERIOD has passed, otherwise it does nothing */ + private fun refreshTellers() { + val lastTellerUpdate = PreferenceManager.getDefaultSharedPreferences(context) + .getLong(Constants.KEY_TELLERS_LAST_UPDATE, 0) + + val now = System.currentTimeMillis() + + if (lastTellerUpdate + Constants.MERCHANTS_UPDATE_PERIOD < now) { + val retrofit = Retrofit.Builder() + .baseUrl(Constants.MERCHANTS_WEBSERVICE_URL) + .addConverterFactory(GsonConverterFactory.create()) + .build() + + val ambassadorService = retrofit.create(MerchantsWebservice::class.java) + val call = ambassadorService.getTellers(0) + call.enqueue(this) + } + } + + override fun onResponse(call: Call>, response: Response>) { + if (response.isSuccessful) { + val res: FeathersResponse? = response.body() + val tellers = res?.data ?: return + insertAllAsyncTask(mTellerDao).execute(tellers) + + val now = System.currentTimeMillis() + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putLong(Constants.KEY_TELLERS_LAST_UPDATE, now).apply() + } + } + + override fun onFailure(call: Call>, t: Throwable) { /* Do nothing */ } + + private class insertAllAsyncTask internal constructor(private val mAsyncTaskDao: TellerDao) : + AsyncTask, Void, Void>() { + + override fun doInBackground(vararg tellers: List): Void? { + mAsyncTaskDao.deleteAll() + mAsyncTaskDao.insertAll(tellers[0]) + return null + } + } +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/TellerMarkerRenderer.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/TellerMarkerRenderer.kt new file mode 100644 index 0000000..b8f6620 --- /dev/null +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/utils/TellerMarkerRenderer.kt @@ -0,0 +1,83 @@ +package cy.agorise.bitsybitshareswallet.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.util.SparseArray +import android.view.ViewGroup +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.model.BitmapDescriptor +import com.google.android.gms.maps.model.BitmapDescriptorFactory +import com.google.android.gms.maps.model.MarkerOptions +import com.google.maps.android.clustering.Cluster +import com.google.maps.android.clustering.ClusterManager +import com.google.maps.android.clustering.view.DefaultClusterRenderer +import com.google.maps.android.ui.IconGenerator +import com.google.maps.android.ui.SquareTextView +import cy.agorise.bitsybitshareswallet.R +import cy.agorise.bitsybitshareswallet.database.entities.Teller + +/** + * This class is used to create custom merchant and merchant cluster icons to show on the map. + */ +class TellerMarkerRenderer(val context: Context?, map: GoogleMap?, clusterManager: ClusterManager?) : + DefaultClusterRenderer(context, map, clusterManager) { + + // Icons used to display merchants and merchants' clusters on the map + private var tellerIcon: BitmapDescriptor + + private val mIcons = SparseArray() + + private val mDensity: Float + private val mIconGenerator = IconGenerator(context) + + init { + tellerIcon = getMarkerIconFromDrawable( + context?.resources?.getDrawable(R.drawable.ic_teller_pin, null)) + + mDensity = context?.resources?.displayMetrics?.density ?: 2.0F + + this.mIconGenerator.setContentView(this.makeSquareTextView(context)) + this.mIconGenerator.setTextAppearance(com.google.maps.android.R.style.amu_ClusterIcon_TextAppearance) + this.mIconGenerator.setBackground(context?.resources?.getDrawable(R.drawable.ic_teller_cluster, null)) + } + + override fun onBeforeClusterItemRendered(item: Teller?, markerOptions: MarkerOptions?) { + markerOptions?.icon(tellerIcon) + } + + override fun onBeforeClusterRendered(cluster: Cluster?, markerOptions: MarkerOptions?) { + val bucket = getBucket(cluster) + var descriptor: BitmapDescriptor? = mIcons.get(bucket) + if (descriptor == null) { + descriptor = BitmapDescriptorFactory.fromBitmap(mIconGenerator.makeIcon(getClusterText(bucket))) + mIcons.put(bucket, descriptor) + } + markerOptions?.icon(descriptor) + } + + override fun shouldRenderAsCluster(cluster: Cluster?): Boolean { + return (cluster?.size ?: 0) > 1 + } + + private fun makeSquareTextView(context: Context?): SquareTextView { + val squareTextView = SquareTextView(context) + val layoutParams = ViewGroup.LayoutParams(-2, -2) + squareTextView.layoutParams = layoutParams + squareTextView.id = com.google.maps.android.R.id.amu_text + val padding = (24.0f * this.mDensity).toInt() + squareTextView.setPadding(padding, padding, padding, padding) + return squareTextView + } + + private fun getMarkerIconFromDrawable(drawable: Drawable?): BitmapDescriptor { + val canvas = Canvas() + val bitmap = Bitmap.createBitmap(drawable?.intrinsicWidth ?: 24, + drawable?.intrinsicHeight ?: 24, Bitmap.Config.ARGB_8888) + canvas.setBitmap(bitmap) + drawable?.setBounds(0, 0, drawable.intrinsicWidth, drawable.intrinsicHeight) + drawable?.draw(canvas) + return BitmapDescriptorFactory.fromBitmap(bitmap) + } +} \ No newline at end of file diff --git a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/MerchantViewModel.kt b/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/MerchantViewModel.kt index edb555d..d37eb72 100644 --- a/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/MerchantViewModel.kt +++ b/app/src/main/java/cy/agorise/bitsybitshareswallet/viewmodels/MerchantViewModel.kt @@ -4,12 +4,19 @@ import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import cy.agorise.bitsybitshareswallet.database.entities.Merchant +import cy.agorise.bitsybitshareswallet.database.entities.Teller import cy.agorise.bitsybitshareswallet.repositories.MerchantRepository +import cy.agorise.bitsybitshareswallet.repositories.TellerRepository class MerchantViewModel(application: Application) : AndroidViewModel(application) { private var mMerchantRepository = MerchantRepository(application) + private var mTellerRepository = TellerRepository(application) internal fun getAllMerchants(): LiveData> { return mMerchantRepository.getAll() } + + internal fun getAllTellers(): LiveData> { + return mTellerRepository.getAll() + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_teller_cluster.png b/app/src/main/res/drawable/ic_teller_cluster.png new file mode 100644 index 0000000000000000000000000000000000000000..6abdc53490589fefc1956564246c0764511aa001 GIT binary patch literal 3276 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 MFa&gw_3_C6038* + + + + + + + + +