Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[23029] Use remote icon implementation #172

Merged
merged 2 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.skedgo.tripkit.ui.map

import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import com.google.android.gms.maps.model.BitmapDescriptorFactory
import com.google.android.gms.maps.model.MarkerOptions
import com.squareup.picasso.Picasso.LoadedFrom
import com.squareup.picasso.Target
import java.lang.ref.WeakReference

class MarkerOptionsTarget(
/**
* If a marker was removed from a map, mutating
* its icon is unnecessary, and
* we should let it be GC-ed quickly as possible.
* That's why a weak reference is used.
*/
private val markerOptionsWeakReference: WeakReference<MarkerOptions>
) : Target {

override fun onBitmapLoaded(bitmap: Bitmap?, from: LoadedFrom?) {
bitmap?.let {
val actualMarkerOptions = markerOptionsWeakReference.get()
val icon = BitmapDescriptorFactory.fromBitmap(it)
actualMarkerOptions?.icon(icon)
} ?: run {
println("bitmap is null")
}
}

override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
val actualMarkerOptions = markerOptionsWeakReference.get()
val fallbackIcon =
BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_YELLOW)
actualMarkerOptions?.icon(fallbackIcon)
}

override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
// Placeholder if needed
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package com.skedgo.tripkit.ui.map

import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.PorterDuff
import android.graphics.PorterDuff.Mode.SRC_IN
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
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.skedgo.tripkit.common.model.stop.ScheduledStop
import com.skedgo.tripkit.common.model.stop.StopType
import com.skedgo.tripkit.routing.ModeInfo
import com.skedgo.tripkit.ui.BuildConfig
import com.skedgo.tripkit.ui.utils.BindingConversions
import com.skedgo.tripkit.ui.utils.DeviceInfo
import com.skedgo.tripkit.ui.utils.StopMarkerUtils.getLocalMapIconUrlForModeInfo
import com.skedgo.tripkit.ui.utils.StopMarkerUtils.getRemoteMapIconUrlForModeInfo
import com.squareup.picasso.Picasso
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import java.lang.ref.WeakReference
import javax.inject.Inject

class RemoteMarkerIconFetcher @Inject constructor(
private val picasso: Picasso
) {

companion object {
const val SIZE_CIRCULAR_BITMAP = 30
const val TINT_BITMAP_RGB = 255
}

fun call(markerOptions: MarkerOptions, modeInfo: ModeInfo?) {
modeInfo?.let {
val url = getRemoteMapIconUrlForModeInfo(DeviceInfo.getDensityDpiName(), it)
picasso.load(url).into(MarkerOptionsTarget(WeakReference(markerOptions)))
}
}

fun callAsync(markerOptions: MarkerOptions, stop: ScheduledStop): Single<MarkerOptions> {
val modeInfo = stop.modeInfo
val iconUrl =
if(modeInfo?.remoteIconIsTemplate == true) {
getRemoteMapIconUrlForModeInfo(DeviceInfo.getDensityDpiName(), modeInfo)
} else {
getLocalMapIconUrlForModeInfo(DeviceInfo.getDensityDpiName(), modeInfo)
}
return Single.defer {
Single.create { emitter ->
picasso.load(iconUrl)
.into(object : com.squareup.picasso.Target {
override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
bitmap?.let {
val circularBitmap = createCircularMarkerBitmap(
it,
Color.rgb(TINT_BITMAP_RGB, TINT_BITMAP_RGB, TINT_BITMAP_RGB),
Color.rgb(
modeInfo?.color?.red ?: 0,
modeInfo?.color?.green ?: 0,
modeInfo?.color?.blue ?: 0
),
SIZE_CIRCULAR_BITMAP
)

val icon = BitmapDescriptorFactory.fromBitmap(circularBitmap)
markerOptions.icon(icon)
emitter.onSuccess(markerOptions)
} ?: run {
emitter.onError(Throwable("Bitmap is null"))
}
}

override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
if(BuildConfig.DEBUG) {
e?.printStackTrace()
}
emitter.onError(e ?: Throwable("Bitmap failed to load"))
}

override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
// Placeholder if needed
}
})
}.onErrorResumeNext {
// Fallback to local resource-based marker icon
getMapIconFromResource(markerOptions, stop.type)
}
}.subscribeOn(AndroidSchedulers.mainThread())
}

private fun getMapIconFromResource(markerOptions: MarkerOptions, type: StopType?): Single<MarkerOptions> {
return Single.fromCallable {
val iconRes = BindingConversions.convertStopTypeToMapIconRes(type)
val icon: BitmapDescriptor = if (iconRes == 0) {
BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_YELLOW)
} else {
BitmapDescriptorFactory.fromResource(iconRes)
}
markerOptions.icon(icon)
markerOptions
}
}

private fun createCircularMarkerBitmap(
bitmap: Bitmap,
tintColor: Int,
circleColor: Int,
circleRadius: Int
): Bitmap {
// Create a new bitmap for the output
val output = Bitmap.createBitmap(circleRadius * 2, circleRadius * 2, Bitmap.Config.ARGB_8888)
val canvas = Canvas(output)

// Draw the white border
val borderPaint = Paint().apply {
isAntiAlias = true
color = Color.WHITE
style = Paint.Style.STROKE
strokeWidth = circleRadius * 0.1f // Border thickness is 10% of the radius
}
canvas.drawCircle(circleRadius.toFloat(), circleRadius.toFloat(), circleRadius.toFloat() - (borderPaint.strokeWidth / 2), borderPaint)

// Draw the circular background inside the border
val backgroundPaint = Paint().apply {
isAntiAlias = true
color = circleColor
}
canvas.drawCircle(circleRadius.toFloat(), circleRadius.toFloat(), circleRadius.toFloat() - borderPaint.strokeWidth, backgroundPaint)

// Apply tint to the bitmap
val tintedBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true)
val bitmapCanvas = Canvas(tintedBitmap)
val tintPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
colorFilter = PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)
}
bitmapCanvas.drawBitmap(tintedBitmap, 0f, 0f, tintPaint)

// Scale the bitmap to fit inside the circle while maintaining the aspect ratio
val aspectRatio = bitmap.width.toFloat() / bitmap.height.toFloat()
val targetWidth: Int
val targetHeight: Int

if (aspectRatio > 1) {
// Landscape orientation: Width is greater than height
targetWidth = circleRadius * 2
targetHeight = (targetWidth / aspectRatio).toInt()
} else if(aspectRatio == 1f) {
targetHeight = circleRadius
targetWidth = circleRadius
} else {
// Portrait orientation: Height is greater than or equal to width
targetHeight = circleRadius * 2
targetWidth = (targetHeight * aspectRatio).toInt()
}

val scaledBitmap = Bitmap.createScaledBitmap(
tintedBitmap,
targetWidth,
targetHeight,
true
)

// Draw the scaled bitmap at the center of the circular background
val left = (output.width - scaledBitmap.width) / 2f
val top = (output.height - scaledBitmap.height) / 2f
canvas.drawBitmap(scaledBitmap, left, top, null)

return output
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.res.Resources
import com.google.android.gms.maps.model.MarkerOptions
import com.skedgo.tripkit.common.model.location.Location
import com.skedgo.tripkit.common.model.stop.ScheduledStop
import com.skedgo.tripkit.data.locations.StopsFetcher
import com.skedgo.tripkit.ui.map.adapter.StopInfoWindowAdapter
import com.skedgo.tripkit.ui.tracking.EventTracker
import com.squareup.otto.Bus
Expand All @@ -20,7 +21,7 @@ class StopPOILocation(
resources: Resources,
picasso: Picasso
): Single<MarkerOptions> {
return scheduledStop.createStopMarkerOptions()
return scheduledStop.createStopMarkerOptions(picasso)
}

override fun getInfoWindowAdapter(context: Context): StopInfoWindowAdapter? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,40 @@ import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MarkerOptions
import com.skedgo.tripkit.common.model.stop.ScheduledStop
import com.skedgo.tripkit.ui.utils.BindingConversions
import com.squareup.picasso.Picasso
import io.reactivex.Single

fun ScheduledStop.createStopMarkerOptions(): Single<MarkerOptions> {
return Single.fromCallable {
val stop = this
val title = stop.getStopDisplayName()

val markerOptions = MarkerOptions()
markerOptions.title(title)
markerOptions.snippet(stop.services)
markerOptions.position(LatLng(stop.lat, stop.lon))
markerOptions.draggable(false)
val stop = this
val title = stop.getStopDisplayName()
val markerOptions = MarkerOptions()
.title(title)
.snippet(stop.services)
.position(LatLng(stop.lat, stop.lon))
.draggable(false)

return Single.fromCallable {
val iconRes = BindingConversions.convertStopTypeToMapIconRes(stop.type)
val icon: BitmapDescriptor
if (iconRes == 0) {
icon = BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_YELLOW)
val icon: BitmapDescriptor = if (iconRes == 0) {
BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_YELLOW)
} else {
icon = BitmapDescriptorFactory.fromResource(iconRes)
BitmapDescriptorFactory.fromResource(iconRes)
}
markerOptions.icon(icon)
markerOptions
}
}
fun ScheduledStop.createStopMarkerOptions(picasso: Picasso): Single<MarkerOptions> {
val stop = this
val title = stop.getStopDisplayName()
val markerOptions = MarkerOptions()
.title(title)
.snippet(stop.services)
.position(LatLng(stop.lat, stop.lon))
.draggable(false)
return kotlin.run {
val remoteMarkerIconFetcher = RemoteMarkerIconFetcher(picasso)
remoteMarkerIconFetcher.callAsync(markerOptions, stop)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ import android.annotation.SuppressLint
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.graphics.Bitmap
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Toast
import com.araujo.jordan.excuseme.ExcuseMe
Expand Down Expand Up @@ -38,16 +36,13 @@ import com.skedgo.tripkit.common.model.region.Region
import com.skedgo.tripkit.common.model.region.Region.City
import com.skedgo.tripkit.common.model.TransportMode
import com.skedgo.tripkit.data.regions.RegionService
import com.skedgo.tripkit.routing.ModeInfo
import com.skedgo.tripkit.routing.VehicleDrawables
import com.skedgo.tripkit.tripplanner.NonCurrentType
import com.skedgo.tripkit.tripplanner.PinUpdate
import com.skedgo.tripkit.ui.R
import com.skedgo.tripkit.ui.TripKitUI
import com.skedgo.tripkit.ui.core.addTo
import com.skedgo.tripkit.ui.core.module.HomeMapFragmentModule
import com.skedgo.tripkit.ui.data.toLocation
import com.skedgo.tripkit.ui.map.BearingMarkerIconBuilder
import com.skedgo.tripkit.ui.map.GenericIMapPoiLocation
import com.skedgo.tripkit.ui.map.IMapPoiLocation
import com.skedgo.tripkit.ui.map.LocationEnhancedMapFragment
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.skedgo.tripkit.ui.utils

import android.content.Context
import com.skedgo.tripkit.common.util.TransportModeUtils

object DeviceInfo {
var densityDpi: Int = 0
private set

fun init(context: Context) {
val metrics = context.resources.displayMetrics
densityDpi = metrics.densityDpi
}

fun getDensityDpiName(): String = TransportModeUtils.getDensityDpiName(densityDpi)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ import com.skedgo.tripkit.configuration.ServerManager

import com.skedgo.tripkit.routing.ModeInfo

//
object StopMarkerUtils {
private val MAP_ICON_URL_TEMPLATE_PRODUCTION =
ServerManager.configuration.staticTripGoUrl + "icons/android/%s/ic_map_%s.png"
private val MAP_ICON_URL_TEMPLATE_PRODUCTION2 =
ServerManager.configuration.staticTripGoUrl + "icons/android/%s/ic_map_marker_%s.png"
private val MAP_ICON_URL_TEMPLATE_BETA =
ServerManager.configuration.bigBangUrl + "modeicons/android/%s/ic_map_%s.png"
private val MAP_ICON_URL_TEMPLATE_STATIC =
ServerManager.configuration.staticTripGoUrl + "icons/android/%s/ic_transport_%s.png"

fun getMapIconUrlForModeInfo(resources: Resources, modeInfo: ModeInfo?): String? {
if (modeInfo == null || modeInfo.remoteIconName == null) {
Expand All @@ -29,6 +32,30 @@ object StopMarkerUtils {
)
}

fun getLocalMapIconUrlForModeInfo(densityDpiName: String, modeInfo: ModeInfo?): String? {
if (modeInfo?.localIconName == null) {
return null
}

return String.format(
MAP_ICON_URL_TEMPLATE_STATIC,
densityDpiName,
modeInfo.localIconName
)
}

fun getRemoteMapIconUrlForModeInfo(densityDpiName: String, modeInfo: ModeInfo?): String? {
if (modeInfo?.remoteIconName == null) {
return null
}

return String.format(
MAP_ICON_URL_TEMPLATE_STATIC,
densityDpiName,
modeInfo.remoteIconName
)
}

fun getMapIconUrlForModeInfo(resources: Resources, remoteIcon: String?): List<String>? {
if (remoteIcon == null) {
return null
Expand Down
Loading