ProviderProxy.kt 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. package ml.adamsprogs.bimba
  2. import android.content.Context
  3. import android.content.Intent
  4. import kotlinx.coroutines.*
  5. import kotlinx.coroutines.android.Main
  6. import ml.adamsprogs.bimba.datasources.VmClient
  7. import ml.adamsprogs.bimba.datasources.VmService
  8. import ml.adamsprogs.bimba.models.Departure
  9. import ml.adamsprogs.bimba.models.Plate
  10. import ml.adamsprogs.bimba.models.StopSegment
  11. import ml.adamsprogs.bimba.models.Timetable
  12. import ml.adamsprogs.bimba.models.suggestions.GtfsSuggestion
  13. import ml.adamsprogs.bimba.models.suggestions.StopSuggestion
  14. import java.util.*
  15. import kotlin.collections.HashMap
  16. //todo make singleton
  17. class ProviderProxy(context: Context? = null) {
  18. private val vmClient = VmClient.getVmClient()
  19. private var timetable: Timetable = Timetable.getTimetable(context)
  20. private var suggestions = emptyList<GtfsSuggestion>()
  21. private val requests = HashMap<String, Request>()
  22. var mode = if (timetable.isEmpty()) MODE_VM else MODE_FULL
  23. companion object {
  24. const val MODE_FULL = "mode_full"
  25. const val MODE_VM = "mode_vm"
  26. }
  27. fun getSuggestions(query: String = "", callback: (List<GtfsSuggestion>) -> Unit) {
  28. GlobalScope.launch {
  29. suggestions = getStopSuggestions(query) //+ getLineSuggestions(query) //todo<p:v+1> + bike stations, train stations, &c
  30. val filtered = filterSuggestions(query)
  31. launch(Dispatchers.Main) {
  32. callback(filtered)
  33. }
  34. }
  35. }
  36. private suspend fun getStopSuggestions(query: String): List<StopSuggestion> {
  37. val vmSuggestions = withContext(Dispatchers.Default) {
  38. vmClient.getStops(query)
  39. }
  40. return if (vmSuggestions.isEmpty() and !timetable.isEmpty()) {
  41. timetable.getStopSuggestions()
  42. } else {
  43. vmSuggestions
  44. }
  45. }
  46. private fun filterSuggestions(query: String): List<GtfsSuggestion> {
  47. return suggestions.filter {
  48. deAccent(it.name).contains(deAccent(query), true)
  49. }
  50. }
  51. private fun deAccent(str: String): String {
  52. var result = str.replace('ę', 'e', true)
  53. result = result.replace('ó', 'o', true)
  54. result = result.replace('ą', 'a', true)
  55. result = result.replace('ś', 's', true)
  56. result = result.replace('ł', 'l', true)
  57. result = result.replace('ż', 'z', true)
  58. result = result.replace('ź', 'z', true)
  59. result = result.replace('ć', 'c', true)
  60. result = result.replace('ń', 'n', true)
  61. return result
  62. }
  63. fun getSheds(name: String, callback: (Map<String, Set<String>>) -> Unit) {
  64. GlobalScope.launch {
  65. val vmSheds = vmClient.getSheds(name)
  66. val sheds = if (vmSheds.isEmpty() and !timetable.isEmpty()) {
  67. timetable.getHeadlinesForStop(name)
  68. } else {
  69. vmSheds
  70. }
  71. launch(Dispatchers.Main) {
  72. callback(sheds)
  73. }
  74. }
  75. }
  76. fun subscribeForDepartures(stopSegments: Set<StopSegment>, listener: OnDeparturesReadyListener, context: Context): String {
  77. stopSegments.forEach {
  78. val intent = Intent(context, VmService::class.java)
  79. intent.putExtra("stop", it.stop)
  80. intent.action = "request"
  81. context.startService(intent)
  82. }
  83. val uuid = UUID.randomUUID().toString()
  84. requests[uuid] = Request(listener, stopSegments)
  85. return uuid
  86. }
  87. fun subscribeForDepartures(stopCode: String, listener: OnDeparturesReadyListener, context: Context): String {
  88. val intent = Intent(context, VmService::class.java)
  89. intent.putExtra("stop", stopCode)
  90. intent.action = "request"
  91. context.startService(intent)
  92. val uuid = UUID.randomUUID().toString()
  93. requests[uuid] = Request(listener, setOf(StopSegment(stopCode, null)))
  94. return uuid
  95. }
  96. private fun constructSegmentDepartures(stopSegments: Set<StopSegment>): Deferred<Map<String, List<Departure>>> {
  97. return GlobalScope.async {
  98. if (timetable.isEmpty())
  99. emptyMap()
  100. else {
  101. timetable.getStopDeparturesBySegments(stopSegments)
  102. }
  103. }
  104. }
  105. private fun filterDepartures(departures: Map<String, List<Departure>>): List<Departure> {
  106. val now = Calendar.getInstance().secondsAfterMidnight()
  107. val lines = HashMap<String, Int>()
  108. val twoDayDepartures = (timetable.getServiceForToday()?.let {
  109. departures[it]
  110. } ?: emptyList()) +
  111. (timetable.getServiceForTomorrow()?.let { service ->
  112. departures[service]!!.map { it.copy().apply { tomorrow = true } }
  113. } ?: emptyList())
  114. return twoDayDepartures
  115. .filter { it.timeTill(now) >= 0 }
  116. .filter {
  117. val existed = lines[it.line] ?: 0
  118. if (existed < 3) {
  119. lines[it.line] = existed + 1
  120. true
  121. } else false
  122. }
  123. }
  124. fun unsubscribeFromDepartures(uuid: String, context: Context) {
  125. requests[uuid]?.unsubscribe(context)
  126. requests.remove(uuid)
  127. }
  128. fun refreshTimetable(context: Context) {
  129. timetable = Timetable.getTimetable(context, true)
  130. mode = MODE_FULL
  131. }
  132. fun getFullTimetable(stopCode: String): Map<String, List<Departure>> {
  133. return if (timetable.isEmpty())
  134. emptyMap()
  135. else
  136. timetable.getStopDepartures(stopCode)
  137. }
  138. fun getFullTimetable(stopSegments: Set<StopSegment>): Map<String, List<Departure>> {
  139. return if (timetable.isEmpty())
  140. emptyMap()
  141. else
  142. timetable.getStopDeparturesBySegments(stopSegments)
  143. }
  144. fun fillStopSegment(stopSegment: StopSegment, callback: (StopSegment?) -> Unit) {
  145. GlobalScope.launch {
  146. callback(fillStopSegment(stopSegment))
  147. }
  148. }
  149. suspend fun fillStopSegment(stopSegment: StopSegment): StopSegment? {
  150. if (stopSegment.plates != null)
  151. return stopSegment
  152. return if (timetable.isEmpty())
  153. vmClient.getDirections(stopSegment.stop)
  154. else
  155. timetable.getHeadlinesForStopCode(stopSegment.stop)
  156. }
  157. fun getStopName(stopCode: String, callback: (String?) -> Unit) {
  158. GlobalScope.launch {
  159. callback(getStopName(stopCode))
  160. }
  161. }
  162. suspend fun getStopName(stopCode: String): String? {
  163. return if (timetable.isEmpty())
  164. vmClient.getName(stopCode)
  165. else
  166. timetable.getStopName(stopCode)
  167. }
  168. fun describeService(service: String, context: Context): String? {
  169. return if (timetable.isEmpty())
  170. null
  171. else
  172. timetable.getServiceDescription(service, context)
  173. }
  174. fun getServiceFirstDay(service: String): Int {
  175. return timetable.getServiceFirstDay(service)
  176. }
  177. interface OnDeparturesReadyListener {
  178. fun onDeparturesReady(departures: List<Departure>, plateId: Plate.ID?, code: Int)
  179. }
  180. inner class Request(private val listener: OnDeparturesReadyListener, private val segments: Set<StopSegment>) : MessageReceiver.OnVmListener {
  181. private val receiver = MessageReceiver.getMessageReceiver()
  182. private val receivedPlates = HashSet<Plate.ID>()
  183. private var cache: Deferred<Map<String, List<Departure>>>? = null
  184. init {
  185. receiver.addOnVmListener(this@Request)
  186. GlobalScope.launch {
  187. cache = constructSegmentDepartures(segments)
  188. }
  189. }
  190. override fun onVm(vmDepartures: Set<Departure>?, plateId: Plate.ID?, stopCode: String, code: Int) {
  191. GlobalScope.launch(Dispatchers.Main) {
  192. if ((plateId == null || vmDepartures == null) and (timetable.isEmpty())) {
  193. listener.onDeparturesReady(emptyList(), null, code)
  194. return@launch
  195. }
  196. if (plateId == null) {
  197. listener.onDeparturesReady(filterDepartures(cache!!.await()), null, code)
  198. } else {
  199. if (segments.any { plateId in it }) {
  200. if (vmDepartures != null) {
  201. listener.onDeparturesReady(vmDepartures.toList(), plateId, code)
  202. if (plateId !in receivedPlates)
  203. receivedPlates.add(plateId)
  204. } else {
  205. receivedPlates.remove(plateId)
  206. if (receivedPlates.isEmpty()) {
  207. listener.onDeparturesReady(filterDepartures(cache!!.await()), null, code)
  208. }
  209. }
  210. }
  211. }
  212. }
  213. }
  214. fun unsubscribe(context: Context) {
  215. segments.forEach {
  216. val intent = Intent(context, VmService::class.java)
  217. intent.putExtra("stop", it.stop)
  218. intent.action = "remove"
  219. context.startService(intent)
  220. }
  221. receiver.removeOnVmListener(this)
  222. }
  223. }
  224. }