Skip to content

Commit

Permalink
Load species on experimentation
Browse files Browse the repository at this point in the history
Signed-off-by: Kyle Corry <kylecorry31@gmail.com>
  • Loading branch information
kylecorry31 committed Jan 3, 2025
1 parent d631146 commit af9cae3
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class FileSubsystem private constructor(private val context: Context) {
return local.getFile(path, create)
}

fun getDirectory(path: String, create: Boolean = false): File {
return local.getDirectory(path, create)
}

fun list(path: String): List<File> {
return local.list(path)
}
Expand Down Expand Up @@ -148,6 +152,11 @@ class FileSubsystem private constructor(private val context: Context) {
get(filename, true)
}

suspend fun createTempDirectory(): File = onIO {
val filename = "${TEMP_DIR}/${UUID.randomUUID()}"
getDirectory(filename, true)
}

fun getLocalPath(file: File): String {
return local.getRelativePath(file)
}
Expand Down
14 changes: 14 additions & 0 deletions app/src/main/java/com/kylecorry/trail_sense/shared/views/Views.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.kylecorry.trail_sense.shared.views

import android.content.Context
import android.net.Uri
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
Expand Down Expand Up @@ -54,4 +56,16 @@ object Views {
}
}

fun image(
context: Context,
uri: Uri,
width: Int = ViewGroup.LayoutParams.WRAP_CONTENT,
height: Int = ViewGroup.LayoutParams.WRAP_CONTENT
): View {
return ImageView(context).apply {
setImageURI(uri)
layoutParams = ViewGroup.LayoutParams(width, height)
}
}

}
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
package com.kylecorry.trail_sense.tools.experimentation

import android.graphics.Bitmap
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.text.util.Linkify
import android.util.Size
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.kylecorry.andromeda.alerts.dialog
import com.kylecorry.andromeda.core.coroutines.BackgroundMinimumState
import com.kylecorry.andromeda.core.coroutines.onIO
import com.kylecorry.andromeda.core.system.Resources
import com.kylecorry.andromeda.fragments.BoundFragment
import com.kylecorry.andromeda.fragments.inBackground
import com.kylecorry.andromeda.views.list.AsyncListIcon
import com.kylecorry.andromeda.views.list.ListItem
import com.kylecorry.trail_sense.databinding.FragmentExperimentationBinding
import com.kylecorry.trail_sense.shared.io.DeleteTempFilesCommand
import com.kylecorry.trail_sense.shared.io.FileSubsystem
import com.kylecorry.trail_sense.shared.views.Views
import com.kylecorry.trail_sense.tools.species_catalog.Species

class ExperimentationFragment : BoundFragment<FragmentExperimentationBinding>() {

private var species by state<Species?>(null)
private var species by state<List<Species>>(emptyList())
private var filter by state("")
private val importer by lazy { SpeciesImportService.create(this) }
private val files by lazy { FileSubsystem.getInstance(requireContext()) }

Expand All @@ -28,33 +35,85 @@ class ExperimentationFragment : BoundFragment<FragmentExperimentationBinding>()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.text.movementMethod = LinkMovementMethod.getInstance()
binding.text.autoLinkMask = Linkify.WEB_URLS
inBackground(BackgroundMinimumState.Created) {
species = importer.import()
val tagOrder = listOf(
"Plant",
"Fungus",
"Mammal",
"Bird",
"Reptile",
"Amphibian",
"Fish",
"Insect",
"Arachnid",
"Crustacean",
"Mollusk",
)
species = (importer.import() ?: emptyList()).sortedWith(
compareBy(
{
it.tags.minOfOrNull { tag ->
val order = tagOrder.indexOf(tag)
if (order == -1) tagOrder.size else order
}
},
{ it.name })
)
}
binding.title.setOnClickListener {
inBackground(BackgroundMinimumState.Created) {
species = importer.import()
// TODO: Ask the user if they want to import them, if they do copy the files to local storage and delete temp files
}

binding.search.setOnSearchListener {
filter = it
}
}

override fun onUpdate() {
super.onUpdate()
effect2(species) {
binding.title.title.text = species?.name
binding.text.text = species?.notes
binding.title.subtitle.text = species?.tags?.joinToString(", ")
if (species?.images?.isNotEmpty() == true) {
binding.image.setImageURI(files.uri(species?.images?.firstOrNull() ?: ""))
} else {
binding.image.setImageBitmap(null)
}
effect2(species, filter) {
binding.list.setItems(species.filter { it.name.lowercase().contains(filter.trim()) }
.map {
val firstSentence = it.notes?.substringBefore(".")?.plus(".") ?: ""
ListItem(
it.id,
it.name,
it.tags.joinToString(", ") + "\n\n" + firstSentence.take(200),
icon = AsyncListIcon(
viewLifecycleOwner,
{ loadThumbnail(it) },
size = 48f,
clearOnPause = true
),
) {
dialog(
it.name,
it.notes ?: "",
allowLinks = true,
contentView = Views.image(
requireContext(),
files.uri(it.images.first()),
width = ViewGroup.LayoutParams.MATCH_PARENT,
height = Resources.dp(requireContext(), 200f).toInt()
),
scrollable = true
)
}
})
}
}

private suspend fun loadThumbnail(species: Species): Bitmap = onIO {
val size = Resources.dp(requireContext(), 48f).toInt()
try {
files.bitmap(species.images.first(), Size(size, size)) ?: getDefaultThumbnail()
} catch (e: Exception) {
getDefaultThumbnail()
}
}

private fun getDefaultThumbnail(): Bitmap {
val size = Resources.dp(requireContext(), 48f).toInt()
return Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
}

override fun onDestroy() {
super.onDestroy()
inBackground {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kylecorry.trail_sense.tools.experimentation

import com.kylecorry.andromeda.files.ZipUtils
import com.kylecorry.andromeda.fragments.AndromedaFragment
import com.kylecorry.andromeda.json.JsonConvert
import com.kylecorry.luna.coroutines.onIO
Expand All @@ -18,31 +19,86 @@ class SpeciesImportService(
private val uriService: UriService,
private val files: FileSubsystem
) :
ImportService<Species> {
override suspend fun import(): Species? = onIO {
ImportService<List<Species>> {

// TODO: Use an enum for tags
private val idToTag = mapOf(
"Africa" to 1,
"Antarctica" to 2,
"Asia" to 3,
"Australia" to 4,
"Europe" to 5,
"North America" to 6,
"South America" to 7,
"Plant" to 8,
"Animal" to 9,
"Fungus" to 10,
"Bird" to 11,
"Mammal" to 12,
"Reptile" to 13,
"Amphibian" to 14,
"Fish" to 15,
"Insect" to 16,
"Arachnid" to 17,
"Crustacean" to 18,
"Mollusk" to 19,
"Forest" to 20,
"Desert" to 21,
"Grassland" to 22,
"Wetland" to 23,
"Mountain" to 24,
"Urban" to 25,
"Marine" to 26,
"Freshwater" to 27,
"Cave" to 28,
"Tundra" to 29,
).map { it.value to it.key }.toMap()

override suspend fun import(): List<Species>? = onIO {
val uri = uriPicker.open(
listOf(
"application/json"
"application/json",
"application/zip"
)
) ?: return@onIO null
val stream = uriService.inputStream(uri) ?: return@onIO null
stream.use {
// TODO: Parse from zip (write images to a temp directory)
return@use parseJson(it)
if (files.getMimeType(uri) == "application/json") {
return@use parseJson(it)
}
return@use parseZip(it)
}
}

private suspend fun parseZip(stream: InputStream): List<Species>? {
val root = files.createTempDirectory()
ZipUtils.unzip(stream, root, MAX_ZIP_FILE_COUNT)

// Parse each file as a JSON file
val species = mutableListOf<Species>()

for (file in root.listFiles() ?: return null) {
if (file.extension == "json") {
species.addAll(parseJson(file.inputStream()) ?: emptyList())
}
}

return species
}

private suspend fun parseJson(stream: InputStream): Species? {
private suspend fun parseJson(stream: InputStream): List<Species>? {
val json = stream.bufferedReader().use { it.readText() }
return try {
val parsed = JsonConvert.fromJson<SpeciesJson>(json) ?: return null
val images = parsed.images.map { saveImage(it) }
Species(
0,
parsed.name,
images,
parsed.tags,
parsed.notes ?: ""
listOf(
Species(
0,
parsed.name,
images,
parsed.tags.mapNotNull { idToTag[it] },
parsed.notes ?: ""
)
)
} catch (e: Exception) {
null
Expand All @@ -61,7 +117,7 @@ class SpeciesImportService(
class SpeciesJson {
var name: String = ""
var images: List<String> = emptyList()
var tags: List<String> = emptyList()
var tags: List<Int> = emptyList()
var notes: String? = null
}

Expand All @@ -73,5 +129,7 @@ class SpeciesImportService(
FileSubsystem.getInstance(fragment.requireContext())
)
}

private const val MAX_ZIP_FILE_COUNT = 10000
}
}
34 changes: 11 additions & 23 deletions app/src/main/res/layout/fragment_experimentation.xml
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">

<LinearLayout
<com.kylecorry.trail_sense.shared.views.SearchView
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:padding="16dp" />

<com.kylecorry.andromeda.views.toolbar.Toolbar
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

<com.kylecorry.andromeda.views.image.AsyncImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="150dp" />

<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp" />


</LinearLayout>
</ScrollView>
<com.kylecorry.andromeda.views.list.AndromedaListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

0 comments on commit af9cae3

Please sign in to comment.