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 0000000..6abdc53 Binary files /dev/null and b/app/src/main/res/drawable/ic_teller_cluster.png differ diff --git a/app/src/main/res/drawable/ic_teller_pin.xml b/app/src/main/res/drawable/ic_teller_pin.xml new file mode 100644 index 0000000..a3854a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_teller_pin.xml @@ -0,0 +1,15 @@ + + + + + + + + + +