Skip to content

Commit

Permalink
Add area issues screen
Browse files Browse the repository at this point in the history
  • Loading branch information
bubelov committed Feb 12, 2024
1 parent 7806ed8 commit 05860a7
Show file tree
Hide file tree
Showing 45 changed files with 397 additions and 13 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 @@ -33,6 +33,7 @@ import user.UsersRepo
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.koin.dsl.bind
import log.LogRecordQueries
import issue.IssuesModel

val appModule = module {
single {
Expand Down Expand Up @@ -86,4 +87,6 @@ val appModule = module {

viewModelOf(::SearchModel)
viewModelOf(::SearchResultModel)

viewModelOf(::IssuesModel)
}
8 changes: 5 additions & 3 deletions app/src/main/kotlin/area/AreaAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ class AreaAdapter(
item.count,
item.count,
)
root.setOnClickListener { listener.onIssuesClick() }
}
}

Expand Down Expand Up @@ -206,12 +207,13 @@ class AreaAdapter(
}

interface Listener {

fun onMapClick()

fun onElementClick(item: Item.Element)

fun onUrlClick(url: HttpUrl)

fun onIssuesClick()

fun onElementClick(item: Item.Element)
}

companion object {
Expand Down
20 changes: 14 additions & 6 deletions app/src/main/kotlin/area/AreaFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,25 @@ class AreaFragment : Fragment() {
}
}

override fun onUrlClick(url: HttpUrl) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url.toString())
startActivity(intent)
}

override fun onIssuesClick() {
findNavController().navigate(
R.id.action_areaFragment_to_issuesFragment,
bundleOf("area_id" to requireArgs().areaId),
)
}

override fun onElementClick(item: AreaAdapter.Item.Element) {
findNavController().navigate(
R.id.elementFragment,
bundleOf("element_id" to item.id),
)
}

override fun onUrlClick(url: HttpUrl) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url.toString())
startActivity(intent)
}
},
)

Expand Down Expand Up @@ -100,6 +107,7 @@ class AreaFragment : Fragment() {
binding.progress.isVisible = true
binding.list.isVisible = false
}

is AreaModel.State.Loaded -> {
val elements =
state.items.filterIsInstance<AreaAdapter.Item.Element>().size
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/kotlin/element/AreaElement.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ data class AreaElement(
val icon: String,
val osmTags: OsmTags,
val issues: JSONArray,
val osmType: String,
val osmId: Long,
)
8 changes: 6 additions & 2 deletions app/src/main/kotlin/element/ElementQueries.kt
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,9 @@ class ElementQueries(private val db: SQLiteOpenHelper) {
lon,
json_extract(tags, '$.icon:android') AS icon_id,
json_extract(osm_json, '$.tags') AS osm_tags,
json_extract(tags, '$.issues') AS issues
json_extract(tags, '$.issues') AS issues,
json_extract(osm_json, '$.type') AS osm_type,
json_extract(osm_json, '$.id') AS osm_id
FROM element
WHERE
deleted_at = ''
Expand All @@ -276,7 +278,9 @@ class ElementQueries(private val db: SQLiteOpenHelper) {
lon = cursor.getDouble(2),
icon = cursor.getString(3),
osmTags = cursor.getJsonObject(4),
issues = cursor.getJsonArray(5)
issues = cursor.getJsonArray(5),
osmType = cursor.getString(6),
osmId = cursor.getLong(7),
)
}
}
Expand Down
79 changes: 79 additions & 0 deletions app/src/main/kotlin/issue/IssuesAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package issue

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import org.btcmap.R
import org.btcmap.databinding.ItemIssueBinding

class IssuesAdapter(
private val listener: Listener,
) : ListAdapter<IssuesAdapter.Item, IssuesAdapter.ItemViewHolder>(DiffCallback()) {

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

return ItemViewHolder(binding)
}

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

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

fun bind(item: Item, listener: Listener) {
binding.apply {
when (item.type) {
"not_verified" -> icon.setImageResource(R.drawable.verified)
"out_of_date" -> icon.setImageResource(R.drawable.schedule)
else -> icon.setImageResource(R.drawable.place)
}

title.text = item.description
subtitle.text = item.elementName

root.setOnClickListener { listener.onItemClick(item) }
}
}
}

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

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

override fun areContentsTheSame(
oldItem: Item,
newItem: Item,
): Boolean {
return newItem == oldItem
}
}

data class Item(
val type: String,
val severity: Int,
val description: String,
val osmUrl: String,
val elementName: String,
)

interface Listener {
fun onItemClick(item: Item)
}
}
90 changes: 90 additions & 0 deletions app/src/main/kotlin/issue/IssuesFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package issue

import android.content.Intent
import android.net.Uri
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.*
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.launch
import org.btcmap.databinding.FragmentIssuesBinding
import org.koin.androidx.viewmodel.ext.android.viewModel

class IssuesFragment : Fragment() {

private val model: IssuesModel by viewModel()

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

private val adapter = IssuesAdapter(object : IssuesAdapter.Listener {
override fun onItemClick(item: IssuesAdapter.Item) {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(item.osmUrl)
startActivity(intent)
}
})

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
_binding = FragmentIssuesBinding.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)

model.setArgs(requireArgs())

viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
model.state.collect {
when (it) {
is IssuesModel.State.ShowingItems -> {
adapter.submitList(it.items)
}

else -> {}
}
}

}
}
}

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

private fun requireArgs(): IssuesModel.Args {
return IssuesModel.Args(requireArguments().getString("area_id")!!)
}
}
74 changes: 74 additions & 0 deletions app/src/main/kotlin/issue/IssuesModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package issue

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import area.AreasRepo
import area.name
import area.polygons
import element.ElementsRepo
import json.toList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import map.boundingBox
import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.GeometryFactory

class IssuesModel(
private val areasRepo: AreasRepo,
private val elementsRepo: ElementsRepo,
private val app: Application,
) : AndroidViewModel(app) {

private val _state: MutableStateFlow<State> = MutableStateFlow(State.Loading)
val state = _state.asStateFlow()

fun setArgs(args: Args) {
viewModelScope.launch {
val area = areasRepo.selectById(args.areaId)!!

val polygons = area.tags.polygons()
val boundingBox = boundingBox(polygons)
val geometryFactory = GeometryFactory()

val elements = elementsRepo.selectByBoundingBox(
minLat = boundingBox.latSouth,
maxLat = boundingBox.latNorth,
minLon = boundingBox.lonWest,
maxLon = boundingBox.lonEast,
).filter { element ->
polygons.any {
val coordinate = Coordinate(element.lon, element.lat)
it.contains(geometryFactory.createPoint(coordinate))
}
}

val issues = elements.map { element ->
val osmUrl = "https://www.openstreetmap.org/${element.osmType}/${element.osmId}"

element.issues.toList().map {
IssuesAdapter.Item(
type = it.getString("type"),
severity = it.getInt("severity"),
description = it.getString("description"),
osmUrl = osmUrl,
elementName = element.osmTags.name(app.resources)
)
}
}.flatten()

_state.update { State.ShowingItems(issues.sortedByDescending { it.severity }) }
}
}

data class Args(val areaId: String)

sealed class State {

data object Loading : State()

data class ShowingItems(val items: List<IssuesAdapter.Item>) : State()
}
}
23 changes: 23 additions & 0 deletions app/src/main/res/layout/fragment_issues.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:navigationIcon="?attr/homeAsUpIndicator"
app:title="@string/issues" />

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" />

</androidx.constraintlayout.widget.ConstraintLayout>
Loading

0 comments on commit 05860a7

Please sign in to comment.