1 /** <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * ``` 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * ``` 10 * 11 * Unless required by applicable law or agreed to in writing, software distributed under the License 12 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 13 * or implied. See the License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 package com.android.healthconnect.controller.shared.map 17 18 import android.content.Context 19 import android.graphics.Canvas 20 import android.graphics.Paint 21 import android.graphics.RectF 22 import android.health.connect.datatypes.ExerciseRoute 23 import android.health.connect.datatypes.ExerciseRoute.Location 24 import android.util.AttributeSet 25 import android.view.View 26 import com.android.settingslib.widget.theme.R 27 import java.lang.Math.toDegrees 28 import java.lang.Math.toRadians 29 import kotlin.math.asin 30 import kotlin.math.atan2 31 import kotlin.math.cos 32 import kotlin.math.max 33 import kotlin.math.min 34 import kotlin.math.sin 35 import kotlin.math.sqrt 36 37 /** A view displaying a path given an exercise route. */ 38 class MapView 39 @JvmOverloads 40 constructor( 41 context: Context, 42 attrs: AttributeSet? = null, 43 defStyleAttr: Int = 0, 44 defStyleRes: Int = 0, 45 ) : View(context, attrs, defStyleAttr, defStyleRes) { 46 47 private val mapBounds: RectF = RectF() 48 private val routeBounds: RectF = 49 RectF(Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MAX_VALUE) 50 private val route: MutableList<Location> = mutableListOf() 51 private val paint: Paint 52 private val startPaint: Paint 53 54 init { 55 val baseColor = context.getColor(R.color.settingslib_text_color_primary) 56 paint = 57 Paint(Paint.ANTI_ALIAS_FLAG).apply { 58 color = baseColor 59 isAntiAlias = true 60 strokeCap = Paint.Cap.ROUND 61 strokeJoin = Paint.Join.ROUND 62 strokeWidth = 4f 63 style = Paint.Style.STROKE 64 } 65 66 startPaint = 67 Paint(Paint.ANTI_ALIAS_FLAG).apply { 68 color = baseColor 69 isAntiAlias = true 70 strokeCap = Paint.Cap.ROUND 71 strokeJoin = Paint.Join.ROUND 72 strokeWidth = 4f 73 style = Paint.Style.FILL_AND_STROKE 74 } 75 76 setWillNotDraw(false) 77 } 78 79 fun setRoute(route: ExerciseRoute) { 80 routeBounds.set(Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_VALUE, Float.MAX_VALUE) 81 this.route.clear() 82 this.route.addAll(route.routeLocations) 83 this.route.sortBy { location -> location.time } 84 invalidate() 85 } 86 87 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 88 super.onMeasure(widthMeasureSpec, heightMeasureSpec) 89 90 val width = MeasureSpec.getSize(widthMeasureSpec) 91 val height = MeasureSpec.getSize(heightMeasureSpec) 92 93 mapBounds.set( 94 width * PADDING, height * PADDING, width * (1 - PADDING), height * (1 - PADDING)) 95 } 96 97 98 override fun onDraw(canvas: Canvas) { 99 super.onDraw(canvas) 100 drawRoute(canvas) 101 setBackgroundColor(context.getColor(R.color.settingslib_colorSurfaceVariant)) 102 } 103 104 private fun drawRoute(canvas: Canvas) { 105 if (route.isEmpty()) { 106 return 107 } 108 val average = average() 109 val adjustedRoute = 110 route 111 .map { location -> 112 var latitude = (location.latitude - average.first + 180) % 180 113 if (latitude > 90) latitude -= 180 114 var longitude = (location.longitude - average.second + 360) % 360 115 if (longitude > 180) longitude -= 360 116 Pair(latitude, longitude) 117 } 118 .toList() 119 120 adjustedRoute.forEach { point -> 121 val lat = point.first.toFloat() 122 val lon = point.second.toFloat() 123 routeBounds.set( 124 min(routeBounds.left, lon), 125 max(routeBounds.top, lat), 126 max(routeBounds.right, lon), 127 min(routeBounds.bottom, lat)) 128 } 129 var previous = translate(adjustedRoute[0]) 130 131 adjustedRoute.forEach { point -> 132 val current = translate(point) 133 canvas.drawLine(previous.first, previous.second, current.first, current.second, paint) 134 previous = current 135 } 136 val start = translate(adjustedRoute[0]) 137 val end = translate(adjustedRoute[adjustedRoute.size - 1]) 138 canvas.drawCircle(start.first, start.second, 4f, startPaint) 139 if (!start.equals(end)) { 140 canvas.drawCircle(end.first, end.second, 4f, startPaint) 141 } 142 } 143 144 private fun translate(point: Pair<Double, Double>): Pair<Float, Float> { 145 val yRatio = (point.first - routeBounds.top) / (routeBounds.bottom - routeBounds.top) 146 val xRatio = (point.second - routeBounds.left) / (routeBounds.right - routeBounds.left) 147 val mapX = xRatio * (mapBounds.right - mapBounds.left) + mapBounds.left 148 val mapY = yRatio * (mapBounds.bottom - mapBounds.top) + mapBounds.top 149 return Pair(mapX.toFloat(), mapY.toFloat()) 150 } 151 152 private fun average(): Pair<Double, Double> { 153 var x = 0.0 154 var y = 0.0 155 var z = 0.0 156 157 route.forEach { location -> 158 x += cos(toRadians(location.latitude)) * cos(toRadians(location.longitude)) 159 y += cos(toRadians(location.latitude)) * sin(toRadians(location.longitude)) 160 z += sin(toRadians(location.latitude)) 161 } 162 val r = sqrt(x * x + y * y + z * z) 163 if (r == 0.0) { 164 return Pair(0.0, 0.0) 165 } 166 return Pair(toDegrees(asin(z / r)), toDegrees(atan2(y, x))) 167 } 168 169 companion object { 170 private const val PADDING = 0.2f 171 } 172 } 173