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