Skip to content

Commit

Permalink
Show places offering delivery
Browse files Browse the repository at this point in the history
  • Loading branch information
bubelov committed May 14, 2024
1 parent 0971bcd commit d18c56e
Show file tree
Hide file tree
Showing 46 changed files with 466 additions and 15 deletions.
3 changes: 3 additions & 0 deletions app/src/main/kotlin/app/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import area.AreasRepo
import conf.ConfQueries
import conf.ConfRepo
import db.Database
import delivery.DeliveryModel
import element.ElementQueries
import element.ElementsRepo
import event.EventQueries
Expand Down Expand Up @@ -77,4 +78,6 @@ val appModule = module {
viewModelOf(::SearchResultModel)

viewModelOf(::IssuesModel)

viewModelOf(::DeliveryModel)
}
77 changes: 77 additions & 0 deletions app/src/main/kotlin/delivery/DeliveryAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package delivery

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import element.Element
import icons.iconTypeface
import org.btcmap.databinding.ItemDeliveryBinding

class DeliveryAdapter(
private val onItemClick: (Item) -> Unit,
) : ListAdapter<DeliveryAdapter.Item, DeliveryAdapter.ItemViewHolder>(DiffCallback()) {

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ItemViewHolder {
val binding = ItemDeliveryBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false,
)

binding.icon.typeface = parent.context.iconTypeface()

return ItemViewHolder(binding)
}

override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {
holder.bind(getItem(position), onItemClick)
}

data class Item(
val element: Element,
val icon: String,
val name: String,
val distanceToUser: String,
)

class ItemViewHolder(
private val binding: ItemDeliveryBinding,
) : RecyclerView.ViewHolder(
binding.root,
) {

fun bind(item: Item, onItemClick: (Item) -> Unit) {
binding.apply {
icon.text = item.icon
name.text = item.name
distance.visibility =
if (item.distanceToUser.isNotEmpty()) View.VISIBLE else View.GONE
distance.text = item.distanceToUser
root.setOnClickListener { onItemClick(item) }
}
}
}

class DiffCallback : DiffUtil.ItemCallback<Item>() {

override fun areItemsTheSame(
oldItem: Item,
newItem: Item,
): Boolean {
return newItem.element.id == oldItem.element.id
}

override fun areContentsTheSame(
oldItem: Item,
newItem: Item,
): Boolean {
return true
}
}
}
87 changes: 87 additions & 0 deletions app/src/main/kotlin/delivery/DeliveryFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package delivery

import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.btcmap.databinding.FragmentDeliveryBinding
import org.koin.androidx.viewmodel.ext.android.activityViewModel
import org.koin.androidx.viewmodel.ext.android.viewModel
import search.SearchResultModel

class DeliveryFragment : Fragment() {

private val model: DeliveryModel by viewModel()

private val resultModel: SearchResultModel by activityViewModel()

private var _binding: FragmentDeliveryBinding? = null
private val binding get() = _binding!!

private val adapter = DeliveryAdapter { item ->
resultModel.element.update { item.element }
findNavController().popBackStack()
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentDeliveryBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { toolbar, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
toolbar.updateLayoutParams<ConstraintLayout.LayoutParams> {
topMargin = insets.top
}
val navBarsInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
binding.list.setPadding(0, 0, 0, navBarsInsets.bottom)
WindowInsetsCompat.CONSUMED
}

binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack()
}

binding.list.layoutManager = LinearLayoutManager(requireContext())
binding.list.adapter = adapter
binding.list.setHasFixedSize(true)

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
model.items.collect { adapter.submitList(it) }
}
}

model.setArgs(requireArgs())
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}

private fun requireArgs(): DeliveryModel.Args {
return DeliveryModel.Args(
userLat = requireArguments().getFloat("userLat").toDouble(),
userLon = requireArguments().getFloat("userLon").toDouble(),
searchAreaId = requireArguments().getLong("searchAreaId"),
)
}
}
114 changes: 114 additions & 0 deletions app/src/main/kotlin/delivery/DeliveryModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package delivery

import android.app.Application
import android.location.Location
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import element.Element
import element.ElementsRepo
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.btcmap.R
import org.osmdroid.util.GeoPoint
import java.text.NumberFormat

class DeliveryModel(
private val app: Application,
private val elementsRepo: ElementsRepo,
) : ViewModel() {

companion object {
private val DISTANCE_FORMAT = NumberFormat.getNumberInstance().apply {
maximumFractionDigits = 1
}
}

private val args = MutableStateFlow<Args?>(null)

private val _items = MutableStateFlow<List<DeliveryAdapter.Item>>(emptyList())
val items = _items.asStateFlow()

init {
viewModelScope.launch {
args.collectLatest { args ->
if (args == null) {
return@collectLatest
}

val unsortedElements = elementsRepo.selectByOsmTagValue(
"delivery",
"yes",
) + elementsRepo.selectByOsmTagValue(
"delivery",
"only",
)

val sortedElements = unsortedElements.sortedBy {
getDistanceInMeters(
startLatitude = args.userLat,
startLongitude = args.userLon,
endLatitude = it.lat,
endLongitude = it.lon,
)
}

_items.update {
sortedElements.map {
it.toAdapterItem(
GeoPoint(
args.userLat,
args.userLon,
)
)
}
}
}
}
}

fun setArgs(args: Args) {
this.args.update { args }
}

private fun Element.toAdapterItem(userLocation: GeoPoint?): DeliveryAdapter.Item {
val distanceStringBuilder = StringBuilder()

if (userLocation != null) {
val elementLocation = GeoPoint(lat, lon)
val distanceKm = userLocation.distanceToAsDouble(elementLocation) / 1000

distanceStringBuilder.apply {
append(DISTANCE_FORMAT.format(distanceKm))
append(" ")
append(app.resources.getString(R.string.kilometers_short))
}
}

return DeliveryAdapter.Item(
element = this,
icon = tags.optString("icon:android").ifBlank { "question_mark" },
name = overpassData.getJSONObject("tags").optString("name").ifBlank { "Unnamed" },
distanceToUser = distanceStringBuilder.toString(),
)
}

private fun getDistanceInMeters(
startLatitude: Double,
startLongitude: Double,
endLatitude: Double,
endLongitude: Double,
): Double {
val distance = FloatArray(1)
Location.distanceBetween(startLatitude, startLongitude, endLatitude, endLongitude, distance)
return distance[0].toDouble()
}

data class Args(
val userLat: Double,
val userLon: Double,
val searchAreaId: Long,
)
}
45 changes: 38 additions & 7 deletions app/src/main/kotlin/element/ElementQueries.kt
Original file line number Diff line number Diff line change
Expand Up @@ -95,21 +95,52 @@ class ElementQueries(val db: SQLiteOpenHelper) {
}
}

fun selectByCategory(category: String): List<Element> {
fun selectByOsmTagValue(tagName: String, tagValue: String): List<Element> {
val cursor = db.readableDatabase.query(
"""
SELECT
id,
overpass_data,
tags,
updated_at,
deleted_at,
ext_lat,
ext_lon
FROM element
WHERE json_extract(tags, '$.category') = ?
WHERE json_extract(overpass_data, '$.tags.$tagName') = ?
""",
arrayOf(category),
arrayOf(tagValue),
)

return buildList {
while (cursor.moveToNext()) {
add(
Element(
id = cursor.getLong(0),
overpassData = cursor.getJsonObject(1),
tags = cursor.getJsonObject(2),
updatedAt = cursor.getString(3)!!,
lat = cursor.getDouble(4),
lon = cursor.getDouble(5),
)
)
}
}
}

fun selectByBtcMapTagValue(tagName: String, tagValue: String): List<Element> {
val cursor = db.readableDatabase.query(
"""
SELECT
id,
overpass_data,
tags,
updated_at,
ext_lat,
ext_lon
FROM element
WHERE json_extract(tags, '$.$tagName') = ?
""",
arrayOf(tagValue),
)

return buildList {
Expand Down Expand Up @@ -143,15 +174,15 @@ class ElementQueries(val db: SQLiteOpenHelper) {
ext_lon,
json_extract(tags, '$.icon:android') AS icon_id,
json_extract(tags, '$.boost:expires') AS boost_expires,
json_extract(osm_json, '$.tags.payment:lightning:requires_companion_app') AS requires_companion_app
json_extract(overpass_data, '$.tags.payment:lightning:requires_companion_app') AS requires_companion_app
FROM element
WHERE
`json_extract(tags, '$.category') NOT IN (${excludedCategories.joinToString { "'$it'" }})
json_extract(tags, '$.category') NOT IN (${excludedCategories.joinToString { "'$it'" }})
AND ext_lat > ?
AND ext_lat < ?
AND ext_lon > ?
AND ext_lon < ?
ORDER BY lat DESC
ORDER BY ext_lat DESC
""",
arrayOf(
minLat,
Expand Down
8 changes: 6 additions & 2 deletions app/src/main/kotlin/element/ElementsRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,12 @@ class ElementsRepo(
return withContext(Dispatchers.IO) { queries.selectBySearchString(searchString) }
}

suspend fun selectByCategory(category: String): List<Element> {
return withContext(Dispatchers.IO) { queries.selectByCategory(category) }
suspend fun selectByOsmTagValue(tagName: String, tagValue: String): List<Element> {
return withContext(Dispatchers.IO) { queries.selectByOsmTagValue(tagName, tagValue) }
}

suspend fun selectByBtcMapTagValue(tagName: String, tagValue: String): List<Element> {
return withContext(Dispatchers.IO) { queries.selectByBtcMapTagValue(tagName, tagValue) }
}

suspend fun selectByBoundingBox(
Expand Down
Loading

0 comments on commit d18c56e

Please sign in to comment.