Untitled

mail@pastecode.io avatarunknown
kotlin
a month ago
39 kB
2
Indexable
Never
package org.transhelp.bykerr.uiRevamp.ui.fragments

import android.Manifest
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.*
import android.location.Location
import android.os.Bundle
import android.text.TextPaint
import android.util.SparseArray
import android.util.TypedValue
import android.view.Gravity
import android.view.MenuItem
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AnticipateInterpolator
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.animation.doOnEnd
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.location.component1
import androidx.core.location.component2
import androidx.core.view.*
import androidx.lifecycle.lifecycleScope
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.UiSettings
import com.google.android.gms.maps.model.*
import com.google.gson.Gson
import com.google.maps.android.clustering.algo.PreCachingAlgorithmDecorator
import com.neovisionaries.ws.client.WebSocket
import kotlinx.coroutines.*
import org.json.JSONObject
import org.transhelp.bykerr.R
import org.transhelp.bykerr.databinding.FragmentRoutesLiveTrackBinding
import org.transhelp.bykerr.databinding.RoundedCardHeaderBinding
import org.transhelp.bykerr.databinding.SessionSocketTimeoutDialogBinding
import org.transhelp.bykerr.uiRevamp.helpers.*
import org.transhelp.bykerr.uiRevamp.helpers.component1
import org.transhelp.bykerr.uiRevamp.helpers.component2
import org.transhelp.bykerr.uiRevamp.helpers.listeners.LoadDataListener
import org.transhelp.bykerr.uiRevamp.lifecycleobserver.GpsDialogLifecycleObserver
import org.transhelp.bykerr.uiRevamp.models.tracking.BusStopTrack
import org.transhelp.bykerr.uiRevamp.models.tracking.TrackPacket
import org.transhelp.bykerr.uiRevamp.models.tracking.TrackResponseAllBus
import org.transhelp.bykerr.uiRevamp.ui.activities.BookTicketActivity
import org.transhelp.bykerr.uiRevamp.ui.activities.HomeActivity.Companion.addGuest
import org.transhelp.bykerr.uiRevamp.ui.activities.NearbyStopsAndLiveTrackActivity
import org.transhelp.bykerr.uiRevamp.ui.activities.ViewRouteTrackingActivity
import java.util.concurrent.atomic.AtomicInteger

/**
 * Fragment view to display live tracking of nearby routes
 */
class RoutesLiveTrackFragment :
    BaseFragmentBinding<FragmentRoutesLiveTrackBinding, NearbyStopsAndLiveTrackActivity>(FragmentRoutesLiveTrackBinding::inflate),
    OnMapReadyCallback, LoadDataListener {
    private lateinit var initialLatLng: LatLng
    private lateinit var currentLatLng: LatLng
    private var mIsActVisible = false
    private var mAnimatedCameraOnce = false
    private var mSocket: WebSocket? = null
    private var mJobResponse: Job? = null
    private var mJobMarkerUpdateRemoveAdd: Job? = null

    private val mGoogleMapLiveData = GoogleMap::class.asLiveData
    private val mCoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
        throwable.printStackTrace()
    }

    // mResponseAllBusObserver - holds initial array vehicle data
    private val mResponseAllBusObserver = TrackResponseAllBus::class.asLiveData

    private val mBinding by  lazy {
        binding
    }
    private val mMarkersSparse = SparseArray<Marker>()

    private val mBusImg by lazy {
        AppUtils.resize(
            AppUtils.bitmapFromVector(baseActivity, R.drawable.ic_bus_tracking_solid),
            16.toDp().toPx().toInt(),
            16.toDp().toPx().toInt()
        )
    }

    private val dp8 by lazy {
        TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, resources.displayMetrics)
    }

    private val dp4 by lazy {
        TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4f, resources.displayMetrics)
    }

    private val bitmapPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        isDither = false
    }
    private val textPaint by lazy {
        TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
            color = Color.WHITE
            isDither = true
            textSize =
                TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12f, resources.displayMetrics)
        }
    }

    private val paddingPaint by lazy {
        Paint(Paint.ANTI_ALIAS_FLAG).apply {
            color = ResourcesCompat.getColor(resources, R.color.colorPrimary, null)
            style = Paint.Style.FILL
        }
    }
    private val textBoundRect = Rect()


    private val mSocketListener by CustomSocketListenerImpl(onConnected = {

//        mBinding.root.postDelayed(3000) {
//            disconnectSocket()
//            val vv = mResponseAllBusObserver.value!!
//
//            mResponseAllBusObserver.postValue(vv.copy(error = "wew"))
//        }
        buildSocketModel {
            channel `is` "1006"
            command `is` SocketModel.COMMAND_INITIATE_FOR_MAP
            currentLatLng.apply {
                latLng `is` SocketModel.LatLong(latitude, longitude)
            }
            send(it, baseActivity.iPreferenceHelper, baseActivity.iprefWrapper.getUserToken())
        }
    }, onMessage = {
        logit("message $it")
//        mJobResponse?.cancel()
        // IO - for running on suspending process
        // handler - for getting any crash recorded to stacktrace and not dismiss current coroutine
        // SuperVisor for not cancelling subsequent coroutine children
        mJobResponse =
            lifecycleScope.launch(Dispatchers.IO + mCoroutineExceptionHandler + SupervisorJob()) {
                val jsonObj = JSONObject(it ?: "")
                val command = jsonObj.optString(SocketModel.COMMAND)
                if (command.isEmpty()) return@launch
                onMain {
                    baseActivity.loadViewModel.isLoaded.postValue(true)
                }
                val gson = Gson()
                if (command.equals(SocketModel.COMMAND_INITIATE_FOR_MAP, true)) {
                    mResponseAllBusObserver.postValue(
                        gson.fromJson(
                            it,
                            TrackResponseAllBus::class.java
                        )
                    )
                } else if (command.equals(SocketModel.COMMAND_RECEIVER, true)) {

                    val packet = gson.fromJson(it, TrackPacket::class.java)
                    val v = packet?.vehicle ?: return@launch

                    val originalData = mResponseAllBusObserver.value ?: return@launch
                    val items = mResponseAllBusObserver.value?.vehicle.orEmpty().toMutableList()
                    val newItem = TrackResponseAllBus.Data(
                        v.latitude.toString(),
                        v.longitude.toString(),
                        v.routeNumber,
                        v.serialNo,
                        v.routeId,
                        v.header,
                        v.title
                    )

                    // find such bus whose serialNo matches with existing list
                    // and then update it contents
                    // if no bus found add in list
                    items.indexOf(newItem).takeIf { it != -1 }?.also {
                        items[it] = newItem
                    } ?: items.add(newItem)
                    mResponseAllBusObserver.postValue(
                        originalData.copy(
                            vehicle = items,
                            error = packet.error
                        )
                    )
                }
            }
    })

    private var updateContainerWidth = 0
    private var updateContainerHeight = 0

    private lateinit var mCardViewRounded: RoundedCardHeaderBinding

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        lifecycle.addObserver(baseActivity.locationLifecycleObserver)
        mBinding.init(savedInstanceState)
        AppUtils.captureCleverTapEventLiveBusTracking(
            baseActivity.clevertapDefaultInstance,
            CleverTapConstants.LIVE_TRACKING__ALL_NEARBY_LIVE_ROUTES_SCREEN_VIEWED,
            baseActivity.iPreferenceHelper.getSelectedCityObject()?.cityName)
    }

    override fun onViewMount() {
        mAnimatedCameraOnce = false
        mMarkersSparse.clear()

        if (this::mCardViewRounded.isInitialized.not() || mBinding.root.contains(mCardViewRounded.root))
            setupHeaderRounded()

        showLottie()
        setupMap()
    }

    override fun onDetach() {
        super.onDetach()
        disconnectSocket()
        mJobResponse?.cancel()
        mJobMarkerUpdateRemoveAdd?.cancel()
        mMarkersSparse.clear()
        mGoogleMapLiveData.value?.clear()
        mResponseAllBusObserver.value = null
        mGoogleMapLiveData.value = null
    }

    override fun onDestroyView() {
        super.onDestroyView()
        try {
            baseActivity.locationLifecycleObserver.let { lifecycle.removeObserver(it) }
        } catch (ex: Exception) {

        }
        mBinding.root.tag = null
        mBinding.socketBinding.root.tag = null
        mBinding.mapView.onDestroy()
//        contentResolver?.unregisterContentObserver(locationObserver)
    }

    private fun setupHeaderRounded() {
        if (this::mCardViewRounded.isInitialized.not())
            mCardViewRounded =
                RoundedCardHeaderBinding.inflate(
                    layoutInflater,
                    mBinding.fragmentViewMapsRoot,
                    false
                )

        val parent = mCardViewRounded.root
        parent.layoutParams =
            LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT).apply {
            leftMargin = 8.dp.px
            gravity = Gravity.CENTER
        }
        parent.isInvisible = true
        if (!mBinding.fragmentViewMapsRoot.contains(parent)) {
            mCardViewRounded.roundedCardHeader.measure(
                View.MeasureSpec.UNSPECIFIED,
                View.MeasureSpec.UNSPECIFIED
            )

            mCardViewRounded.roundedCardHeader.radius = parent.measuredHeight / 2f
            mCardViewRounded.refreshCard.radius = mBinding.socketBinding.rootCard.height / 2f
            mBinding.fragmentViewMapsRoot.addView(parent)

//            val c = mBinding.fragmentViewMapsRoot
//
//            ConstraintSet().apply {
//                clone(c)
//                connect(parent.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, 16.dp.px)
//                connect(
//                    parent.id, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END,
//                    16.dp.px
//                )
//
//                applyTo(c)
//            }
        }
    }

    override fun onAttach(context: Context) {
        super.onAttach(context)

        onActivity {
            GpsDialogLifecycleObserver(this) {
                // todo: check if other classes need below logic
                if (isFinishing || isDestroyed || it.not()) return@GpsDialogLifecycleObserver
                checkLoadData()
            }
        }
    }

    private fun hideLottie(){
        if(mBinding.mapLoaderLottie.isVisible.not()) return
        with(mBinding.mapLoaderLottie) {
            cancelAnimation()
            isVisible = false
        }
    }

    private fun showLottie(){
        with(mBinding.mapLoaderLottie) {
            setAnimation(R.raw.map_live_track_loader)
            progress = 0f
            playAnimation()
            isVisible = true
        }
    }

    private var ivRefreshAnimator: ObjectAnimator? = null
    private val maxRetriesForGuest = AtomicInteger(2)
    private fun FragmentRoutesLiveTrackBinding.init(savedInstanceState: Bundle?) {
        mapView.onCreate(savedInstanceState)
        tvSearchByRoute.setOnClickListener {
            startActivity(Intent(baseActivity, BookTicketActivity::class.java))
        }
        dismissNoTrack.setOnClickListener {
            baseActivity.finish()
        }
        socketBinding.root.setOnClickListener {
            if (root.tag == true) {
                AppUtils.captureCleverTapEventLiveBusTracking(
                    baseActivity.clevertapDefaultInstance,
                    CleverTapConstants.LIVE_TRACKING__REFRESH_BUTTON_CLICKED_FOR_FETCHING_LIVE_ROUTES,
                    baseActivity.iPreferenceHelper.getSelectedCityObject()?.cityName)

                ivRefreshAnimator?.cancel()
                ivRefreshAnimator = ObjectAnimator.ofFloat(
                    mBinding.socketBinding.ivRefresh,
                    View.ROTATION,
                    0f,
                    360f
                ).apply {
                    duration = 1000
                    repeatMode = ValueAnimator.RESTART
                    repeatCount = ValueAnimator.INFINITE
                    interpolator = AccelerateDecelerateInterpolator()
                    start()
                }
                mBinding.socketBinding.collapseOrHide(true) {
                    // wait like ~~~~~~ for 2s
                    mBinding.socketBinding.root.postDelayed(2000) {
                        setupSocket()
                    }
                }
            }
        }

        gpsCardView.setOnClickListener {
            val lastLoc =
                baseActivity.locationLifecycleObserver.locationLiveData.value ?: return@setOnClickListener
            val map = mGoogleMapLiveData.value ?: return@setOnClickListener
            val latLng = LatLng(lastLoc.latitude, lastLoc.longitude)
//            mCircleMap?.center = latLng
            currentLatLng = latLng
            map.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, MIN_ZOOM))
        }

        root.post {
            logit("gawd initial ${socketBinding.root.height}, ${socketBinding.rootCard.height}")
            socketBinding.rootCard.radius =
                socketBinding.root.height / 2f
            updateContainerWidth = socketBinding.root.width
            updateContainerHeight = socketBinding.root.height
            mapView.getMapAsync(this@RoutesLiveTrackFragment)
        }
    }

    private val dp56 by lazy {
        resources.getDimensionPixelSize(R.dimen.dp_56)
    }


    val cornerRadiusAm by lazy {

        ObjectAnimator.ofFloat(
            mBinding.socketBinding.root,
            "radius",
            updateContainerHeight / 2f,
            16f
        ).apply {
            duration = 1000
        }
    }
    val cornerRadiusAm2 by lazy {

        ObjectAnimator.ofFloat(
            mBinding.socketBinding.refreshCard,
            "radius",
            updateContainerHeight / 2f,
            16f
        ).apply {
            duration = 1000
        }
    }

    private fun SessionSocketTimeoutDialogBinding.collapseOrHide(
        hide: Boolean,
        onComplete: () -> Unit = {},
    ) {
        if (root.tag == hide) return
        root.tag = hide

        val scaleTo: Float
        val scaleFrom: Float

        val heightTo: Int
        val heightFrom: Int

        val widthTo: Int
        val widthFrom: Int

        if (hide) {
            scaleTo = 0.8f
            scaleFrom = 1f

            heightTo = dp56
            heightFrom = updateContainerHeight

            widthTo = dp56
            widthFrom = updateContainerWidth
        } else {
            scaleFrom = 0.8f
            scaleTo = 1f

            heightFrom = dp56
            heightTo = updateContainerHeight

            widthFrom = dp56
            widthTo = updateContainerWidth
        }
        val anm = AnimatorSet()

//        val scaleAm = ValueAnimator.ofFloat(scaleFrom, scaleTo).apply {
//            addUpdateListener {
//                val v = it.animatedValue as Float
//                root.scaleX = v
//                root.scaleY = v
//                root.requestLayout()
//            }
//            doOnEnd {
//                cornerRadiusAm.start()
//            }
//        }
        val widthAm = ValueAnimator.ofInt(widthFrom, widthTo).apply {
            addUpdateListener {
                val v = it.animatedValue as Int
                val prog = it.animatedFraction

                root.updateLayoutParams {
                    width = v
                }
//                if (prog == 1f) {
//                    cornerRadiusAm.start()
//                    cornerRadiusAm2.start()
//                }
//                if (prog > 0.6f && scaleAm.isStarted.not()) {
//                    scaleAm.start()
//                }
            }
        }

        val heightAm = ValueAnimator.ofInt(heightFrom, heightTo).apply {
            addUpdateListener {
                val v = it.animatedValue as Int
                root.updateLayoutParams {
                    height = v
                }
            }
        }

        anm.apply {
            duration = 400
            interpolator = AnticipateInterpolator()
            playTogether(widthAm, heightAm)
            doOnEnd {
                onComplete()
                root.requestLayout()
                if (widthTo == dp56) {

                }
            }
            start()
        }
    }

    private suspend fun isMarkerInBounds(
        newLatLng: LatLng,
    ) = withContext(Dispatchers.Main) {
        latLngBoundsScreen.contains(
            newLatLng
        )
    }

    private suspend fun isMarkerInBounds(
        marker: Marker,
    ) = withContext(Dispatchers.Main) {
        latLngBoundsScreen.contains(
            marker.position
        )
    }

    private suspend fun isMarkerInVisibleRegion(
        newLatLng: LatLng,
        googleMap: GoogleMap?,
    ): Boolean {
//        googleMap?.projection?.toScreenLocation(newLatLng)
        return withContext(Dispatchers.Main) {
            googleMap?.projection?.visibleRegion?.latLngBounds?.contains(
                newLatLng
            ) ?: false
        }
    }

//    private var mCircleMap: Circle? = null

    private val errorResponse by lazy {
        getString(R.string.socket_401)
    }

    @SuppressLint("PotentialBehaviorOverride")
    private fun setupMap() {
//        locationLifecycleObserver.locationLiveData.observe(this) {
//            if (it == null) return@observe
//            val (lat, lng) = it
//            val latLng = LatLng(lat, lng)
//            val (x, y) = mGoogleMapLiveData.value?.projection?.toScreenLocation(latLng)
//                ?: return@observe
//            mBinding.wew.x = x.toFloat() - (mBinding.wew.height / 2f)
//            // subtract extra toolbar height since map parent has toolbar and `y` here is from root parent
//            mBinding.wew.y = y.toFloat() - (mBinding.wew.height / 2f  - mBinding.toolbarBinding.root.height)
//
//            logit("Wew ${mBinding.wew.y}, ${mBinding.wew.x}")
//            if(mBinding.wew.y >= 0f && mBinding.wew.x >= 0f)
//                mBinding.wew.invalidate()
//        }

        mGoogleMapLiveData.observe(viewLifecycleOwner) {
            if(it == null) return@observe
            setupSocket()
        }

        onActivity {
            mResponseAllBusObserver.observe(viewLifecycleOwner) { responseItem ->
                logit("Response $responseItem")
                if(responseItem == null) return@observe
                val error = responseItem.error
                if (error.isNullOrEmpty().not() && mBinding.root.tag == null) {
                    val isAuthError = error?.equals(errorResponse, true) == true
//                mBinding.ivQuit.performClick()
                    if (isAuthError) {
                            clearLoggedOutUserSession(true,::setupSocket)
                            return@observe
                    }
                    mBinding.socketBinding.root.animate().alpha(1f).withEndAction {
                        mBinding.socketBinding.collapseOrHide(false)
                        mBinding.root.tag = true
                        disconnectSocket()
                    }.start()
                    return@observe
                }

                val googleMap = mGoogleMapLiveData.value ?: return@observe
                if (mIsActVisible.not()) return@observe

                val response = when {
                    responseItem.data.isNullOrEmpty().not() -> responseItem.data
                    responseItem.vehicle.isNullOrEmpty().not() -> responseItem.vehicle
                    else -> null
                }

                if(mCardViewRounded.root.isInvisible){
                    mCardViewRounded.root.isVisible = true
                }
                mCardViewRounded.time = AppUtils.getCurrentTimehhmmss()
                lifecycleScope.launch(Dispatchers.Default + mCoroutineExceptionHandler) {
                    for (it in response.orEmpty()) {
                        if (mIsActVisible.not()) return@launch
                        addOrAnimateMarker(it, googleMap)

//                if (mAnimatedCameraOnce.not()) {
//                    locationLiveData.value?.let {
//                        val (latitude, longitude) = it
//                        val current = LatLng(latitude, longitude)
//
//                        withContext(Dispatchers.Main) {
//                            map.animateCamera(CameraUpdateFactory.newLatLng(current))
//                            mAnimatedCameraOnce = true
//
//                        }
//                    }
//                }
                        // todo: test garbage collector doesn't creates too much racing condition in gpu and cpu
                        //   by default hardware acceleration is enabled, performance may degrade if buffer swap is high
                        System.gc()
                    }
                }
            }
        }
    }

    private suspend fun addOrAnimateMarker(
        it: TrackResponseAllBus.Data,
        googleMap: GoogleMap,
        checkVisibleBounds: Boolean = false,
    ) {
        if (it.routeNumber.isEmpty()) return
        val serialNo = it.serialNo
        val imeiHash = serialNo.hashCode()
        val oldMkr = mMarkersSparse.get(imeiHash)

        val newLatLng = LatLng(it.latitude.toDouble(), it.longitude.toDouble())

        // 1. Look for marker in memory
        // 2. If exist
        //  a. Update if in visible region
        //  b. Update if not in transition with clustering
        // 3. Else
        // 4. Add to sparseArray
        // 5. Cluster all sparseArray
        if (newLatLng.latitude != 0.0 || newLatLng.longitude != 0.0) {
            (oldMkr != null).let { has ->
                if (has) {
                    onMain {
                        val oldPos = oldMkr.position
                        if (oldPos == newLatLng) return@onMain
                        logit("marker $serialNo from $oldPos to $newLatLng")
                        ValueAnimator.ofObject(
                            LatLngEvaluator(),
                            oldPos,
                            newLatLng,
                        ).apply {
                            duration = 2.msFromSec
                            addUpdateListener {
                                oldMkr.position = it.animatedValue as LatLng
                            }
                            start()
                        }
                    }
                } else {
                    MarkerOptions().apply {
                        position(newLatLng)
                        snippet(
                            JSONObject()
                                .put("routeId", it.routeId)
                                .put("routeNumber", it.routeNumber)
                                .toString()
                        )
//                            icon(getBitmapMarker(it.routeNumber))
                        icon(getBitmapMarkerFromView(it.routeNumber.trim()))
                    }.apply {
                        onMain {
                            mMarkersSparse[imeiHash] = googleMap.addMarker(this)

                            logit("Added marker to pool $it")
                        }
                    }
                }
            }
        }
    }

    private fun disconnectSocket() {
        logit("Socket disc....")
        mSocket?.disconnect()
        mSocket?.removeListener(mSocketListener)
        mSocket = null
    }

    override fun onStop() {
        super.onStop()
        mIsActVisible = false
        mBinding.mapView.onStop()
        disconnectSocket()
    }

    override fun onStart() {
        super.onStart()
        mIsActVisible = true
        mBinding.mapView.onStart()
        if (mGoogleMapLiveData.value != null)
            setupSocket()

//        if(isGPSEnabled().not())
//            showGPSEnablePopup()
    }


    private fun setupSocket() = onActivity{
        hideLottie()
        if(isUserOutOfBounds){
            logit("Gawd")
            return
        }
        if (isGPSEnabled().not() || this@RoutesLiveTrackFragment::currentLatLng.isInitialized.not()) return
        if(ConnectivityManagerHelper.isNetworkAvailable(applicationContext).not()) {
            loadViewModel.isLoaded.postValue(false)
            return
        }

        logit("Creating socket")

        if (mBinding.root.tag == true) {
            ivRefreshAnimator?.cancel()
            ivRefreshAnimator = null
            mBinding.root.tag = null
            mBinding.socketBinding.root.animate().alpha(0f).start()
        }

        if (mSocket != null) disconnectSocket()
        mSocket = setupLiveTrackSocket().apply {
            addListener(mSocketListener)
            connectAsynchronously()
        }
    }

    companion object {
        private const val MIN_ZOOM = 16f
        private const val MAX_DISTANCE_TO_FETCH_IN_METRE = 250
        private const val INITIAL_ZOOM = 16f
        private const val INITIAL_CLUSTER_CLICK_ZOOM = 3f
    }

    private lateinit var latLngBoundsScreen: LatLngBounds
    private var isUserOutOfBounds = true

    override fun onMapReady(map: GoogleMap) {
        map.setMapStyle(
            MapStyleOptions.loadRawResourceStyle(
                baseActivity, R.raw.style_json
            )
        )

        val uiSettings: UiSettings? = map.uiSettings
        uiSettings?.apply {
            setAllGesturesEnabled(false)
            isCompassEnabled = false
            isMyLocationButtonEnabled = false
            isMapToolbarEnabled = false
            isZoomGesturesEnabled = true
            isScrollGesturesEnabled = true
        }

        map.setMinZoomPreference(MIN_ZOOM)
        val locationCurrent =
            baseActivity.locationLifecycleObserver.locationLiveData.value
        logit("Loc from int $locationCurrent")
        locationCurrent?.also {
            val (lat, lng) = it
            val latLng = LatLng(lat, lng)
            initialLatLng = latLng
            currentLatLng = latLng
            mAnimatedCameraOnce = true
            map.animateCamera(
                CameraUpdateFactory.newLatLng(
                    latLng
                )
            )
        }
//        p0.moveCamera(CameraUpdateFactory.newLatLngZoom(p0.cameraPosition.target, INITIAL_ZOOM))

        map.isMyLocationEnabled = ActivityCompat.checkSelfPermission(
            baseActivity,
            Manifest.permission.ACCESS_FINE_LOCATION
        ) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
            baseActivity,
            Manifest.permission.ACCESS_COARSE_LOCATION
        ) == PackageManager.PERMISSION_GRANTED


        baseActivity.iPreferenceHelper.getSelectedCityObject()?.cityName?.lowercase()
            ?.let { state ->
                val bounds = AppUtils.getCityLatLngBounds(state, baseActivity.getCityModelFromRemoteConfig()) ?: return@let
                if(this::initialLatLng.isInitialized) {
                    isUserOutOfBounds = bounds.contains(initialLatLng).not()
                    // todo: needs testing
                    mBinding.unableToShowLiveTrack.text = getString(R.string.unable_to_show_live_routes_nearby_looks_like_you_are_out_of_b_s_b, state?.capitalize())
                    mBinding.noTrackView.isVisible = isUserOutOfBounds
                }
                map.setLatLngBoundsForCameraTarget(bounds)
            }

        map.setOnMapLoadedCallback {
            mBinding.socketBinding.collapseOrHide(true) {
                baseActivity.locationLifecycleObserver.locationLiveData.value?.also {
                    val (lat, lng) = it
                    val latLng = LatLng(lat, lng)
                    if (this::initialLatLng.isInitialized.not())
                        initialLatLng = latLng
                    if (this::currentLatLng.isInitialized.not())
                        currentLatLng = latLng
                    latLngBoundsScreen = LatLngBounds.builder().run {
                        val (x, y, width, height) = mBinding.layoutCircle
                        // symmetric margins
                        val margins = mBinding.layoutCircle.marginLeft

                        include(
                            map.projection.fromScreenLocation(Point(x + margins, y - dp56)).apply {

                            })
                        map.addCircle(CircleOptions().apply {
                            this.center(latLng)
                            this.radius(AppConstants.DEFAULT_RADIUS_IN_METERS)
                            this.fillColor(
                                ContextCompat.getColor(
                                    baseActivity,
                                    R.color.bg_green_light
                                )
                            )
                            this.strokeColor(Color.BLACK)
                            this.strokeWidth(1f)
                        })
                        include(
                            map.projection.fromScreenLocation(
                                Point(
                                    x + (width - margins),
                                    y + (height - margins)
                                )
                            )
                        )
                        build()
                    }
                    logit("bounds $latLngBoundsScreen, ${latLngBoundsScreen.contains(latLng)}")
                    mGoogleMapLiveData.value?.clear()
                    setupMarkerListener(map)
                    if(isUserOutOfBounds.not())
                    mGoogleMapLiveData.postValue(map)
                }
            }
        }
    }

    private var mDidCameraMove = false

    /**
     * default algo for clustering [PreCachingAlgorithmDecorator]
     */
    @SuppressLint("PotentialBehaviorOverride")
    private fun setupMarkerListener(map: GoogleMap) {
        map.setOnMarkerClickListener {
            AppUtils.captureCleverTapEventLiveBusTracking(
                baseActivity.clevertapDefaultInstance,
                CleverTapConstants.LIVE_TRACKING__BUS_ROUTE_MAP_MARKER_CLICKED,
                baseActivity.iPreferenceHelper.getSelectedCityObject()?.cityName)

            logit("marker clicked ${it.snippet}")
            val snip = it.snippet ?: return@setOnMarkerClickListener true
            val obj = JSONObject(snip)
            val routeNumber = obj.optString("routeNumber")
            val routeId = obj.getString("routeId")
            if (routeNumber.isNullOrEmpty()) {
                return@setOnMarkerClickListener true
            }
            ViewRouteTrackingActivity.start(
                baseActivity,
                routeId,
                routeNumber
            )
            true
        }

        map.setOnCameraMoveStartedListener {
            logit("Wew moving $it")
            mDidCameraMove =
                it == GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE || it == GoogleMap.OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION
        }

        map.setOnCameraMoveCanceledListener {
            mDidCameraMove = false
        }

        val maxDistanceAllowed = 0.05f // 500m
        map.setOnCameraMoveListener {
            val targetLat = map.cameraPosition.target.latitude
            val targetLng = map.cameraPosition.target.longitude
            lifecycleScope.launch(Dispatchers.Default) {

                // distance in metres
                BusStopTrack.getDistance(
                    targetLat,
                    targetLng,
                    initialLatLng.latitude,
                    initialLatLng.longitude
                ).toFloat().takeIf {
                    it <= maxDistanceAllowed
                }?.let { distance ->
                    onMain {
                        mBinding.imgLocationPinUp.apply {
                            scaleX = distance.div(maxDistanceAllowed)
                            scaleY = distance.div(maxDistanceAllowed)
                        }
                    }
                }
//                logit("gawd $distance")
            }
        }
        map.setOnCameraIdleListener {
            logit("Wew Moved $mDidCameraMove")
            if (!mDidCameraMove) return@setOnCameraIdleListener

            mBinding.mapView.postDelayed(2000){

                synchronized(this) {
                    mJobMarkerUpdateRemoveAdd?.cancel()
                    mJobMarkerUpdateRemoveAdd = lifecycleScope.launch(Dispatchers.Default) {
                        if (isActive.not()) return@launch
                        onMain {
                            val targetLat = map.cameraPosition.target.latitude
                            val targetLng = map.cameraPosition.target.longitude
                            val movedDistanceInMet = BusStopTrack.getDistance(
                                currentLatLng.latitude,
                                currentLatLng.longitude,
                                targetLat,
                                targetLng
                            ).times(1000)
                            currentLatLng = map.cameraPosition.target
                            if (movedDistanceInMet > MAX_DISTANCE_TO_FETCH_IN_METRE) {
                                setupSocket()
                            }
                        }

                        // animates marker if they are updated
//                    val responseItem = mResponseAllBusObserver.value ?: return@launch
//                    val response = when {
//                        responseItem.data.isNullOrEmpty().not() -> responseItem.data
//                        responseItem.vehicle.isNullOrEmpty().not() -> responseItem.vehicle
//                        else -> return@launch
//                    }
//                    val visibleRoutes = response.orEmpty().filter {
//                        isMarkerInVisibleRegion(
//                            LatLng(
//                                it.latitude.toDouble(),
//                                it.longitude.toDouble()
//                            ), map
//                        )
//                    }
//
//                    for (data in visibleRoutes) {
//                        if (isActive.not()) break
//                        addOrAnimateMarker(data, map, true)
//                    }
                    }
                }
            }
//            mMarkersSparse.removeAt(mMarkersSparse.indexOfValue(it))
//            lifecycleScope.launch(Dispatchers.Default) {
//                mMarkersSparse.valueIterator().asSequence().toList().filter {
//                    isMarkerInVisibleRegion(it.position, mGoogleMapLiveData.value).not()
//                }.let {
//                    mClusterManager.removeItems(it)
//                }
//            }
        }
    }
    
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (item.itemId == android.R.id.home) {
            baseActivity.onBackPressed()
        }
        return super.onOptionsItemSelected(item)
    }

    private val defaultClusterItemView by lazy {
        layoutInflater.inflate(R.layout.default_cluster_item, mBinding.root, false)
    }

    //    private fun getBitmapMarkerFromView(busNo: String): BitmapDescriptor {
//        val v = defaultClusterItemView
//        v.findViewById<TextView>(R.id.busNo).text = busNo
//        v.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
//
//        val b = Bitmap.createBitmap(v.measuredWidth, v.measuredHeight, Bitmap.Config.ARGB_8888)
//        val c = Canvas(b)
//        v.layout(0, 0, v.measuredWidth, v.measuredHeight)
//        v.draw(c)
//        return BitmapDescriptorFactory.fromBitmap(b)
//    }
    private fun getBitmapMarkerFromView(busNo: String): BitmapDescriptor = run {
        defaultClusterItemView.findViewById<TextView>(R.id.busNo).text = busNo
        defaultClusterItemView.measure(
            View.MeasureSpec.UNSPECIFIED,
            View.MeasureSpec.UNSPECIFIED
        )

        BitmapDescriptorFactory.fromBitmap(
            Bitmap.createBitmap(
                defaultClusterItemView.measuredWidth,
                defaultClusterItemView.measuredHeight,
                Bitmap.Config.ARGB_8888
            ).apply {

                val c = Canvas(this)
                defaultClusterItemView.layout(
                    0,
                    0,
                    defaultClusterItemView.measuredWidth,
                    defaultClusterItemView.measuredHeight
                )
                defaultClusterItemView.draw(c)
            }
        )
    }

    private fun getBitmapMarker(busNo: String): BitmapDescriptor {
        val busImg = mBusImg ?: return BitmapDescriptorFactory.defaultMarker()
        textPaint.getTextBounds(busNo, 0, busNo.length, textBoundRect)
        val (textWidth, textHeight) = textBoundRect.width() to textBoundRect.height()
        val (imgWidth, imgHeight) = busImg.width to busImg.height

        val roundedBgRect = Rect(
            0,
            dp4.toInt(),
            dp8.toInt() + imgWidth + dp8.toInt() + textWidth + dp4.toInt(),
            imgHeight + dp4.toInt() + dp4.toInt() + dp4.toInt()
        )

        val bitmap =
            Bitmap.createBitmap(
                roundedBgRect.width(),
                roundedBgRect.height(),
                Bitmap.Config.ARGB_8888
            )
        val canvas = Canvas(bitmap)
        canvas.drawRoundRect(
            RectF(
                0f,
                0f,
                roundedBgRect.width().toFloat(),
                roundedBgRect.height().toFloat()
            ),
            dp4,
            dp4,
            paddingPaint
        )
        canvas.drawBitmap(busImg, dp4, dp4, bitmapPaint)
        canvas.drawText(
            busNo,
            dp4 + dp4.div(2) + imgWidth + dp4,
            textHeight.div(2f) + roundedBgRect.height().div(2),
            textPaint
        )

        logit("Size alloc ${bitmap.allocationByteCount / 1024f} KB, for route $busNo, canvas ${canvas.height}, ${canvas.width}")
        return BitmapDescriptorFactory.fromBitmap(bitmap)
    }

    override fun checkLoadData() {
        logit("socket Gawd")
        if (mGoogleMapLiveData.value != null) setupSocket()
    }
}