new "mini" marker variant to avoid clustering for zoom levels 11-13

This commit is contained in:
johan12345
2022-06-12 19:36:06 +02:00
parent 93787fae74
commit aa5c36d1aa
9 changed files with 173 additions and 74 deletions

View File

@@ -126,6 +126,7 @@ class GoingElectricApiWrapper(
baseurl: String = "https://api.goingelectric.de",
context: Context? = null
) : ChargepointApi<GEReferenceData> {
private val clusterThreshold = 11f
val api = GoingElectricApi.create(apikey, baseurl, context)
override fun getName() = "GoingElectric.de"
@@ -173,7 +174,7 @@ class GoingElectricApiWrapper(
val categories = formatMultipleChoice(categoriesVal)
// do not use clustering if filters need to be applied locally.
val useClustering = zoom < 13
val useClustering = zoom < clusterThreshold
val geClusteringAvailable = minConnectors == null || minConnectors <= 1
val useGeClustering = useClustering && geClusteringAvailable
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
@@ -267,7 +268,7 @@ class GoingElectricApiWrapper(
val categories = formatMultipleChoice(categoriesVal)
// do not use clustering if filters need to be applied locally.
val useClustering = zoom < 13
val useClustering = zoom < clusterThreshold
val geClusteringAvailable = minConnectors == null || minConnectors <= 1
val useGeClustering = useClustering && geClusteringAvailable
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
@@ -330,7 +331,7 @@ class GoingElectricApiWrapper(
}.map { it.convert(apikey, false) }
// apply clustering
val useClustering = zoom < 13
val useClustering = zoom < clusterThreshold
val geClusteringAvailable = minConnectors == null || minConnectors <= 1
val clusterDistance = if (useClustering) getClusterDistance(zoom) else null
if (!geClusteringAvailable && useClustering) {

View File

@@ -105,6 +105,7 @@ class OpenChargeMapApiWrapper(
baseurl: String = "https://api.openchargemap.io/v3/",
context: Context? = null
) : ChargepointApi<OCMReferenceData> {
private val clusterThreshold = 11
val api = OpenChargeMapApi.create(apikey, baseurl, context)
override fun getName() = "OpenChargeMap.org"
@@ -238,7 +239,7 @@ class OpenChargeMapApiWrapper(
}.map { it.convert(referenceData, false) }.distinct() as List<ChargepointListItem>
// apply clustering
val useClustering = zoom < 13
val useClustering = zoom < clusterThreshold
if (useClustering) {
val clusterDistance = getClusterDistance(zoom)
Dispatchers.IO.run {

View File

@@ -518,7 +518,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = fav == null
fav = fav == null,
mini = vm.useMiniMarkers.value == true
)
)
}
@@ -585,6 +586,9 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
updateMap(chargepoints)
}
})
vm.useMiniMarkers.observe(viewLifecycleOwner) {
vm.chargepoints.value?.data?.let { updateMap(it) }
}
vm.favorites.observe(viewLifecycleOwner, Observer {
updateFavoriteToggle()
})
@@ -650,7 +654,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
mini = vm.useMiniMarkers.value == true
)
)
}
@@ -665,10 +670,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = true,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
mini = vm.useMiniMarkers.value == true
)
)
animator.animateMarkerBounce(marker)
animator.animateMarkerBounce(marker, vm.useMiniMarkers.value == true)
// un-highlight all other markers
markers.forEach { (m, c) ->
@@ -679,7 +685,8 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = false,
fault = c.faultReport != null,
multi = c.isMulti(vm.filteredConnectors.value),
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
fav = c.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
mini = vm.useMiniMarkers.value == true
)
)
}
@@ -823,15 +830,22 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
map.uiSettings.setRotateGesturesEnabled(prefs.mapRotateGesturesEnabled)
map.setIndoorEnabled(false)
map.uiSettings.setIndoorLevelPickerEnabled(false)
map.setOnCameraIdleListener {
vm.mapPosition.value = MapPosition(
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
)
binding.scaleView.update(map.cameraPosition.zoom, map.cameraPosition.target.latitude)
vm.reloadChargepoints()
}
map.setOnCameraMoveListener {
vm.mapPosition.value = MapPosition(
map.projection.visibleRegion.latLngBounds, map.cameraPosition.zoom
)
}
vm.mapPosition.observe(viewLifecycleOwner) {
binding.scaleView.update(map.cameraPosition.zoom, map.cameraPosition.target.latitude)
}
map.setOnCameraMoveStartedListener { reason ->
if (reason == AnyMap.OnCameraMoveStartedListener.REASON_GESTURE) {
if (vm.myLocationEnabled.value == true) {
@@ -1034,9 +1048,11 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight = highlight,
fault = charger.faultReport != null,
multi = charger.isMulti(vm.filteredConnectors.value),
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
fav = charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList(),
mini = vm.useMiniMarkers.value == true
)
)
marker.setAnchor(0.5f, if (vm.useMiniMarkers.value == true) 0.5f else 1f)
}
if (chargers.toSet() != markers.values) {
@@ -1054,7 +1070,10 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
val multi = charger.isMulti(vm.filteredConnectors.value)
val fav =
charger.id in vm.favorites.value?.map { it.charger.id } ?: emptyList()
animator.animateMarkerDisappear(marker, tint, highlight, fault, multi, fav)
animator.animateMarkerDisappear(
marker, tint, highlight, fault, multi, fav,
vm.useMiniMarkers.value == true
)
} else {
animator.deleteMarker(marker)
}
@@ -1082,12 +1101,16 @@ class MapFragment : Fragment(), OnMapReadyCallback, MapsActivity.FragmentCallbac
highlight,
fault,
multi,
fav
fav,
vm.useMiniMarkers.value == true
)
)
.anchor(0.5f, 1f)
.anchor(0.5f, if (vm.useMiniMarkers.value == true) 0.5f else 1f)
)
animator.animateMarkerAppear(
marker, tint, highlight, fault, multi, fav,
vm.useMiniMarkers.value == true
)
animator.animateMarkerAppear(marker, tint, highlight, fault, multi, fav)
markers[marker] = charger
}
}

View File

@@ -46,6 +46,7 @@ class ChargerIconGenerator(
val context: Context,
val factory: BitmapDescriptorFactory?,
val scaleResolution: Int = 20,
val scaleResolutionMini: Int = 10,
val oversize: Float = 1f, // increase to add padding for scale > 1
val height: Int = 48
) {
@@ -56,16 +57,21 @@ class ChargerIconGenerator(
val highlight: Boolean,
val fault: Boolean,
val multi: Boolean,
val fav: Boolean
val fav: Boolean,
val mini: Boolean
)
// 230 items: (21 sizes, 5 colors, multi on/off) + highlight + fault (only with scale = 1)
private val cacheSize = (scaleResolution + 3) * 5 * 2;
// 340 items:
// large: (21 sizes, 5 colors, multi on/off) + highlight + fault + fav (only with scale = 1)
// mini: (11 sizes, 5 colors) + highlight (only with scale = 1)
private val cacheSize = (scaleResolution + 8) * 5 * 2 + (scaleResolutionMini + 2) * 5;
private val cache = LruCache<BitmapData, BitmapDescriptor>(cacheSize)
private val icon = R.drawable.ic_map_marker_charging
private val multiIcon = R.drawable.ic_map_marker_charging_multiple
private val highlightIcon = R.drawable.ic_map_marker_highlight
private val miniIcon = R.drawable.ic_map_marker_charging_mini
private val highlightIcon = R.drawable.ic_map_marker_charging_highlight
private val highlightIconMulti = R.drawable.ic_map_marker_charging_highlight_multiple
private val highlightIconMini = R.drawable.ic_map_marker_charging_highlight_mini
private val faultIcon = R.drawable.ic_map_marker_fault
private val favIcon = R.drawable.ic_map_marker_fav
@@ -82,12 +88,15 @@ class ChargerIconGenerator(
for (highlight in listOf(false, true)) {
for (multi in listOf(false, true)) {
for (fav in listOf(false, true)) {
for (tint in tints) {
for (scale in 0..scaleResolution) {
getBitmapDescriptor(
tint, scale.toFloat() / scaleResolution,
255, highlight, fault, multi, fav
)
for (mini in listOf(false, true)) {
for (tint in tints) {
val scaleRes = if (mini) scaleResolutionMini else scaleResolution
for (scale in 0..scaleRes) {
getBitmapDescriptor(
tint, scale.toFloat() / scaleRes,
255, highlight, fault, multi, fav, mini
)
}
}
}
}
@@ -103,16 +112,10 @@ class ChargerIconGenerator(
highlight: Boolean = false,
fault: Boolean = false,
multi: Boolean = false,
fav: Boolean = false
fav: Boolean = false,
mini: Boolean = false
): BitmapDescriptor? {
val data = BitmapData(
tint, (scale * scaleResolution).roundToInt(),
alpha,
if (scale == 1f) highlight else false,
if (scale == 1f) fault else false,
multi,
if (scale == 1f) fav else false
)
val data = createBitmapData(tint, scale, alpha, highlight, fault, multi, fav, mini)
val cachedImg = cache[data]
return if (cachedImg != null) {
cachedImg
@@ -124,6 +127,26 @@ class ChargerIconGenerator(
}
}
private fun createBitmapData(
tint: Int,
scale: Float,
alpha: Int,
highlight: Boolean,
fault: Boolean,
multi: Boolean,
fav: Boolean,
mini: Boolean
) = BitmapData(
tint,
(scale * (if (mini) scaleResolutionMini else scaleResolution)).roundToInt(),
alpha,
if (scale == 1f) highlight else false,
if (scale == 1f && !mini) fault else false,
if (!mini) multi else false,
if (scale == 1f && !mini) fav else false,
mini
)
fun getBitmap(
@ColorRes tint: Int,
scale: Float = 1f,
@@ -131,38 +154,40 @@ class ChargerIconGenerator(
highlight: Boolean = false,
fault: Boolean = false,
multi: Boolean = false,
fav: Boolean = false
fav: Boolean = false,
mini: Boolean = false
): Bitmap {
val data = BitmapData(
tint, (scale * scaleResolution).roundToInt(),
alpha,
if (scale == 1f) highlight else false,
if (scale == 1f) fault else false,
multi,
if (scale == 1f) fav else false,
)
val data = createBitmapData(tint, scale, alpha, highlight, fault, multi, fav, mini)
return generateBitmap(data)
}
private fun generateBitmap(data: BitmapData): Bitmap {
val icon = if (data.multi) multiIcon else icon
val icon = if (data.mini) miniIcon else if (data.multi) multiIcon else icon
val vd: Drawable = ContextCompat.getDrawable(context, icon)!!
DrawableCompat.setTint(vd, ContextCompat.getColor(context, data.tint));
DrawableCompat.setTintMode(vd, PorterDuff.Mode.MULTIPLY);
DrawableCompat.setTint(vd, ContextCompat.getColor(context, data.tint))
DrawableCompat.setTintMode(vd, PorterDuff.Mode.MULTIPLY)
val density = context.resources.displayMetrics.density
val markerWidth =
(height.toFloat() * density / vd.intrinsicHeight * vd.intrinsicWidth).roundToInt()
val markerHeight = (height * density).roundToInt()
val extraIconSize = (0.75 * markerWidth).roundToInt()
val extraIconShift = (0.25 * markerWidth).roundToInt()
val (markerWidth, markerHeight) = if (data.mini) {
vd.intrinsicWidth to vd.intrinsicHeight
} else {
(height.toFloat() * density / vd.intrinsicHeight * vd.intrinsicWidth).roundToInt() to
(height * density).roundToInt()
}
val (extraIconSize, extraIconShift) = if (data.mini) 0 to 0 else {
(0.75 * markerWidth).roundToInt() to (0.25 * markerWidth).roundToInt()
}
val totalWidth = markerWidth + 2 * extraIconShift
val totalHeight = markerHeight + extraIconShift
val leftPadding = ((totalWidth) * (oversize - 1) / 2).roundToInt() + extraIconShift
val topPadding = ((totalHeight) * (oversize - 1)).roundToInt() + extraIconShift
val (leftPadding, topPadding) = if (!data.mini) {
((totalWidth) * (oversize - 1) / 2).roundToInt() + extraIconShift to
((totalHeight) * (oversize - 1)).roundToInt() + extraIconShift
} else {
0 to 0
}
vd.setBounds(
leftPadding, topPadding,
leftPadding + markerWidth,
@@ -176,18 +201,21 @@ class ChargerIconGenerator(
)
val canvas = Canvas(bm)
val scale = data.scale.toFloat() / scaleResolution
canvas.scale(
scale,
scale,
canvas.width / 2f,
canvas.height.toFloat()
)
val scale = data.scale.toFloat() / if (data.mini) scaleResolutionMini else scaleResolution
val (originX, originY) = if (data.mini) {
canvas.width / 2f to
canvas.height / 2f
} else {
canvas.width / 2f to
canvas.height.toFloat()
}
canvas.scale(scale, scale, originX, originY)
vd.draw(canvas)
if (data.highlight) {
val hIcon = if (data.multi) highlightIconMulti else highlightIcon
val hIcon =
if (data.mini) highlightIconMini else if (data.multi) highlightIconMulti else highlightIcon
val highlightDrawable = ContextCompat.getDrawable(context, hIcon)!!
highlightDrawable.setBounds(
leftPadding, topPadding,
@@ -198,7 +226,7 @@ class ChargerIconGenerator(
highlightDrawable.draw(canvas)
}
if (data.fault) {
if (data.fault && !data.mini) {
val faultDrawable = ContextCompat.getDrawable(context, faultIcon)!!
faultDrawable.setBounds(
leftPadding + markerWidth + extraIconShift - extraIconSize,
@@ -210,7 +238,7 @@ class ChargerIconGenerator(
faultDrawable.draw(canvas)
}
if (data.fav) {
if (data.fav && !data.mini) {
val favDrawable = ContextCompat.getDrawable(context, favIcon)!!
val favShiftY = extraIconShift
val favShiftX = if (data.fault) extraIconShift - extraIconSize else extraIconShift

View File

@@ -38,7 +38,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
highlight: Boolean,
fault: Boolean,
multi: Boolean,
fav: Boolean
fav: Boolean,
mini: Boolean
) {
animatingMarkers[marker]?.let {
it.cancel()
@@ -57,7 +58,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
highlight = highlight,
fault = fault,
multi = multi,
fav = fav
fav = fav,
mini = mini
)
)
}
@@ -77,7 +79,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
highlight: Boolean,
fault: Boolean,
multi: Boolean,
fav: Boolean
fav: Boolean,
mini: Boolean
) {
animatingMarkers[marker]?.let {
it.cancel()
@@ -96,7 +99,8 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
highlight = highlight,
fault = fault,
multi = multi,
fav = fav
fav = fav,
mini = mini
)
)
}
@@ -120,7 +124,7 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
marker.remove()
}
fun animateMarkerBounce(marker: Marker) {
fun animateMarkerBounce(marker: Marker, mini: Boolean) {
animatingMarkers[marker]?.let {
it.cancel()
animatingMarkers.remove(marker)
@@ -131,7 +135,7 @@ class MarkerAnimator(val gen: ChargerIconGenerator) {
interpolator = BounceInterpolator()
addUpdateListener { state ->
val t = max(1f - state.animatedValue as Float, 0f) / 2
marker.setAnchor(0.5f, 1.0f + t)
marker.setAnchor(0.5f, (if (mini) 0.5f else 1.0f) + t)
}
addListener(onEnd = {
animatingMarkers.remove(marker)

View File

@@ -35,9 +35,7 @@ data class MapPosition(val bounds: LatLngBounds, val zoom: Float) : Parcelable
internal fun getClusterDistance(zoom: Float): Int? {
return when (zoom) {
in 0.0..7.0 -> 100
in 7.0..11.5 -> 75
in 11.5..12.5 -> 60
in 12.5..13.0 -> 45
in 7.0..11.0 -> 75
else -> null
}
}
@@ -298,6 +296,29 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
chargepointLoader(Triple(pos, filters, referenceData))
}
private val miniMarkerThreshold = 13f
private val clusterThreshold = 11f
val useMiniMarkers: LiveData<Boolean> = MediatorLiveData<Boolean>().apply {
for (source in listOf(filteredMinPower, mapPosition)) {
addSource(source) {
val minPower = filteredMinPower.value ?: 0
val zoom = mapPosition.value?.zoom
value = when {
zoom == null -> {
false
}
minPower >= 100 -> {
// when only showing high-power chargers we can use large markers
zoom < clusterThreshold
}
else -> {
zoom < miniMarkerThreshold
}
}
}
}
}.distinctUntilChanged()
private var chargepointLoader =
throttleLatest(
500L,
@@ -342,7 +363,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
if (connectorsVal.all) null else connectorsVal.values.map {
GEChargepoint.convertTypeFromGE(it)
}.toSet()
filteredMinPower.value = filters.getSliderValue("minPower")
filteredMinPower.value = filters.getSliderValue("min_power")
} else if (api is OpenChargeMapApiWrapper) {
val connectorsVal = filters.getMultipleChoiceValue("connectors")!!
filteredConnectors.value =
@@ -352,7 +373,7 @@ class MapViewModel(application: Application, private val state: SavedStateHandle
refData as OCMReferenceData
)
}.toSet()
filteredMinPower.value = filters.getSliderValue("minPower")
filteredMinPower.value = filters.getSliderValue("min_power")
} else {
filteredConnectors.value = null
filteredMinPower.value = null

View File

@@ -0,0 +1,9 @@
<vector android:viewportHeight="24"
android:viewportWidth="24"
android:width="16dp"
android:height="16dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" />
</vector>

View File

@@ -0,0 +1,12 @@
<vector android:viewportHeight="24"
android:viewportWidth="24"
android:width="16dp"
android:height="16dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="#dddddd"
android:pathData="M12,12m-8.5,0a8.5,8.5 0,1 1,17 0a8.5,8.5 0,1 1,-17 0" />
<path
android:fillColor="@android:color/white"
android:pathData="M12,12m-7.5,0a7.5,7.5 0,1 1,15 0a7.5,7.5 0,1 1,-15 0" />
</vector>