Skip to content

Commit

Permalink
Merge pull request #172 from skedgo/bugfix/23029-handle-remote-icons
Browse files Browse the repository at this point in the history
[23029] Use remote icon implementation
  • Loading branch information
MichaelReyes authored Jan 20, 2025
2 parents e5633bc + 7ddf5e6 commit cc00038
Show file tree
Hide file tree
Showing 7 changed files with 286 additions and 19 deletions.
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

0 comments on commit cc00038

Please sign in to comment.