@@ -100,154 +100,3 @@ class SearchWidget : GlanceAppWidget() {
100100class SearchWidgetReceiver : GlanceAppWidgetReceiver () {
101101 override val glanceAppWidget: GlanceAppWidget = SearchWidget ()
102102}
103-
104- // ======================= the train map =======================
105-
106- /* * The train network diagram with every train on it, live. */
107- class TrainWidget : GlanceAppWidget () {
108- override val sizeMode = SizeMode .Exact
109-
110- override suspend fun provideGlance (context : Context , id : GlanceId ) {
111- val trains = TrainsNow .get(context)
112- val map = drawTrains(trains ? : emptyList())
113- provideContent { GlanceTheme { Body (context, map, trains) } }
114- }
115-
116- @Composable
117- private fun Body (ctx : Context , map : Bitmap , trains : List <Train >? ) {
118- Column (GlanceModifier .fillMaxSize().cornerRadius(24 .dp).background(ColorProvider (Color (0xFF0B1220 ))).padding(10 .dp)
119- .clickable(open(ctx, " akllive://trains" ))) {
120- Row (GlanceModifier .fillMaxWidth(), verticalAlignment = Alignment .CenterVertically ) {
121- Text (" Ngā Tereina" , GlanceModifier .defaultWeight(),
122- style = TextStyle (color = ColorProvider (Color .White ), fontWeight = FontWeight .Bold , fontSize = 14 .sp))
123- Text (when {
124- trains == null -> " Open the app first"
125- else -> " ${trains.size} running · ${Nz .time(TrainsNow .at)} "
126- }, style = TextStyle (color = ColorProvider (Color (0xFFB9C2CF )), fontSize = 11 .sp))
127- Spacer (GlanceModifier .width(8 .dp))
128- Text (" ↻" , GlanceModifier .clickable(actionRunCallback<RefreshMore >()),
129- style = TextStyle (color = ColorProvider (Color .White ), fontWeight = FontWeight .Bold , fontSize = 18 .sp))
130- }
131- Spacer (GlanceModifier .height(4 .dp))
132- Image (ImageProvider (map), " Train map" , GlanceModifier .fillMaxSize(), contentScale = ContentScale .Fit )
133- }
134- }
135- }
136-
137- class TrainWidgetReceiver : GlanceAppWidgetReceiver () {
138- override val glanceAppWidget: GlanceAppWidget = TrainWidget ()
139- override fun onEnabled (context : Context ) { super .onEnabled(context); Glance .schedule(context) }
140- }
141-
142- /* * The latest trains, shared by the app and the widget. */
143- object TrainsNow {
144- @Volatile var list: List <Train >? = null
145- private set
146- @Volatile var at = 0L
147- private set
148- private var lastWidget = 0L
149- private var repo: TrainRepo ? = null
150-
151- /* * The app polled the trains: keep them, and redraw the widget now and then. */
152- fun save (ctx : Context , trains : List <Train >) {
153- list = trains
154- at = Nz .nowSec()
155- val now = System .currentTimeMillis()
156- if (now - lastWidget > 60_000 ) {
157- lastWidget = now
158- val app = ctx.applicationContext
159- CoroutineScope (Dispatchers .Default ).launch { try { TrainWidget ().updateAll(app) } catch (_: Exception ) { } }
160- }
161- }
162-
163- /* * The app's been opened: a good time to draw the widget's map, with the internet sure to be there. */
164- fun appOpened (ctx : Context ) {
165- val now = System .currentTimeMillis()
166- if (now - lastOpen < 10 * 60_000 ) return
167- lastOpen = now
168- val app = ctx.applicationContext
169- CoroutineScope (Dispatchers .Default ).launch { try { TrainWidget ().updateAll(app) } catch (_: Exception ) { } }
170- }
171- private var lastOpen = 0L
172-
173- /* * Fetch and redraw in a job that waits for the internet. */
174- fun refresh (ctx : Context ) {
175- val req = OneTimeWorkRequestBuilder <TrainWidgetWorker >()
176- .setConstraints(Constraints .Builder ().setRequiredNetworkType(NetworkType .CONNECTED ).build())
177- .build()
178- WorkManager .getInstance(ctx).enqueueUniqueWork(" train-widget-refresh" , ExistingWorkPolicy .REPLACE , req)
179- }
180-
181- suspend fun get (ctx : Context , fresh : Boolean = false): List <Train >? {
182- if (! fresh && list != null && Nz .nowSec() - at < 60 ) return list
183- val s = Prefs (ctx).load()
184- if (s.apiKey.isBlank()) return list
185- val r = repo ? : TrainRepo (AtApi { Prefs (ctx).load().apiKey }).also { repo = it }
186- return try { r.poll().also { list = it; at = Nz .nowSec() } } catch (_: Exception ) { list }
187- }
188- }
189-
190- /* * The post-CRL diagram and every train on it, drawn for the widget. */
191- private fun drawTrains (trains : List <Train >): Bitmap {
192- val k = 2.4f
193- val w = (MapData .W * k).toInt()
194- val h = (MapData .H * k).toInt()
195- val bmp = Bitmap .createBitmap(w, h, Bitmap .Config .ARGB_8888 )
196- val c = Canvas (bmp)
197- val line = Paint (Paint .ANTI_ALIAS_FLAG ).apply { style = Paint .Style .STROKE ; strokeWidth = 3.2f * k; strokeCap = Paint .Cap .ROUND ; strokeJoin = Paint .Join .ROUND }
198- MapData .LINE_DRAW .forEachIndexed { li, pts ->
199- val p = Path ()
200- for (i in 0 until pts.size / 2 ) {
201- val x = pts[i * 2 ] * k
202- val y = pts[i * 2 + 1 ] * k
203- if (i == 0 ) p.moveTo(x, y) else p.lineTo(x, y)
204- }
205- line.color = MapData .LINE_COLORS [li].toInt()
206- c.drawPath(p, line)
207- }
208- val dot = Paint (Paint .ANTI_ALIAS_FLAG )
209- val ring = Paint (Paint .ANTI_ALIAS_FLAG ).apply { style = Paint .Style .STROKE ; strokeWidth = 1.2f * k; color = 0xFF1A2744 .toInt() }
210- val label = Paint (Paint .ANTI_ALIAS_FLAG ).apply { color = 0xFFE6ECF5 .toInt(); textSize = 6.5f * k; typeface = Typeface .DEFAULT_BOLD }
211- for (st in MapData .STATIONS ) {
212- for (i in 0 until st.pts.size / 2 ) {
213- dot.color = 0xFFFFFFFF .toInt()
214- c.drawCircle(st.pts[i * 2 ] * k, st.pts[i * 2 + 1 ] * k, 2.2f * k, dot)
215- c.drawCircle(st.pts[i * 2 ] * k, st.pts[i * 2 + 1 ] * k, 2.2f * k, ring)
216- }
217- if (st.priority == 0 ) {
218- val tw = label.measureText(st.name)
219- val x = (st.x * k + st.dx * k).let { if (st.anchor == ' r' ) it - tw else if (st.anchor == ' t' || st.anchor == ' b' ) it - tw / 2 else it }
220- c.drawText(st.name, x.coerceIn(2f , w - tw - 2f ), st.y * k + st.dy * k + label.textSize / 3 , label)
221- }
222- }
223- for (t in trains) {
224- val x = t.x * k
225- val y = t.y * k
226- dot.color = 0xFFFFFFFF .toInt()
227- c.drawCircle(x, y, 4.6f * k, dot)
228- dot.color = MapData .LINE_COLORS [t.line].toInt()
229- c.drawCircle(x, y, 3.4f * k, dot)
230- val late = t.delay ? : 0
231- if (late >= 120 ) {
232- dot.color = if (late >= 300 ) 0xFFE0412F .toInt() else 0xFFEFA20E .toInt()
233- c.drawCircle(x + 3.4f * k, y - 3.4f * k, 1.8f * k, dot)
234- }
235- }
236- return bmp
237- }
238-
239- /* * ↻ on the train widget: fetched in a job, which gets the internet even when the app's in the background. */
240- class RefreshMore : ActionCallback {
241- override suspend fun onAction (context : Context , glanceId : GlanceId , parameters : ActionParameters ) {
242- TrainsNow .refresh(context)
243- }
244- }
245-
246- /* * Fetches the trains and redraws the train widget. */
247- class TrainWidgetWorker (ctx : Context , params : WorkerParameters ) : CoroutineWorker(ctx, params) {
248- override suspend fun doWork (): Result {
249- TrainsNow .get(applicationContext, fresh = true )
250- TrainWidget ().updateAll(applicationContext)
251- return Result .success()
252- }
253- }
0 commit comments