v1 commit

This commit is contained in:
SaiD 2025-11-28 21:34:31 +05:30
parent 7f24f1cae1
commit 1861d14a7f
24 changed files with 1998 additions and 643 deletions

View File

@ -13,6 +13,9 @@
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="release">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@ -27,6 +27,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("debug")
}
}
compileOptions {

View File

@ -51,6 +51,11 @@
android:exported="false"
android:theme="@style/Theme.AnimalRating" />
<activity
android:name=".RatingActivity"
android:exported="false"
android:theme="@style/Theme.AnimalRating" />
</application>
</manifest>

View File

@ -2,6 +2,7 @@ package com.example.animalrating
import android.Manifest
import android.content.ContentValues
import android.content.Intent
import android.content.pm.ActivityInfo
import android.graphics.Bitmap
import android.graphics.BitmapFactory
@ -12,10 +13,12 @@ import android.os.Bundle
import android.provider.MediaStore
import android.util.Log
import android.util.Size
import android.view.View
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
@ -49,11 +52,15 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
private var isPhotoTaken = false
private var matchThreshold = 75
private var algorithm = HomeActivity.ALGORITHM_HAMMING
private var isAutoCapture = true
private var isMaskDisplayEnabled = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
StringProvider.initialize(this)
cowName = intent.getStringExtra("COW_NAME")
orientation = intent.getStringExtra("ORIENTATION")
@ -61,6 +68,8 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
matchThreshold = prefs.getInt("THRESHOLD", 75)
algorithm = prefs.getString("ALGORITHM", HomeActivity.ALGORITHM_HAMMING) ?: HomeActivity.ALGORITHM_HAMMING
isAutoCapture = prefs.getBoolean(HomeActivity.PREF_AUTO_CAPTURE, true)
isMaskDisplayEnabled = prefs.getBoolean(HomeActivity.PREF_MASK_DISPLAY, false)
// Set orientation based on selected view
if (orientation == "front" || orientation == "back") {
@ -78,16 +87,56 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
finish()
}
val btnShutter = findViewById<MaterialButton>(R.id.btnShutter)
val btnToggle = findViewById<MaterialButton>(R.id.btnToggleCaptureMode)
btnShutter.setOnClickListener {
takePhoto()
}
updateCaptureModeUI()
btnToggle.setOnClickListener {
isAutoCapture = !isAutoCapture
// Save preference for persistence if desired, or just toggle for session
prefs.edit().putBoolean(HomeActivity.PREF_AUTO_CAPTURE, isAutoCapture).apply()
updateCaptureModeUI()
}
frameProcessor = FrameProcessor()
val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0)
overlay.setSilhouette(silhouetteId)
loadSavedMask()
// Need to wait for layout to get width/height for scaling mask correctly
savedMaskOverlay.post {
loadSavedMask()
}
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
private fun updateCaptureModeUI() {
val btnShutter = findViewById<MaterialButton>(R.id.btnShutter)
val btnToggle = findViewById<MaterialButton>(R.id.btnToggleCaptureMode)
if (isAutoCapture) {
btnShutter.visibility = View.GONE
// Auto Mode: Eye Icon, Dark Background
btnToggle.setIconResource(android.R.drawable.ic_menu_view)
btnToggle.setIconTintResource(android.R.color.white)
btnToggle.setBackgroundColor(Color.parseColor("#6D4C41"))
btnToggle.alpha = 1.0f
} else {
btnShutter.visibility = View.VISIBLE
// Manual Mode: Eye Icon (Grey/Transparent) - Requested to keep Eye icon
btnToggle.setIconResource(android.R.drawable.ic_menu_view)
btnToggle.setIconTintResource(android.R.color.darker_gray)
btnToggle.setBackgroundColor(Color.TRANSPARENT)
btnToggle.alpha = 0.7f
}
}
private fun takePhoto() {
if (isPhotoTaken) return
isPhotoTaken = true
@ -96,15 +145,22 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
val name = cowName ?: "unknown"
val side = orientation ?: "unknown"
val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0)
// Find current count for this cow and orientation
val existingFiles = filesDir.listFiles { _, fname ->
val cowFolder = StorageUtils.getCowImageFolder(name)
val existingFiles = cowFolder.listFiles { _, fname ->
fname.startsWith("${name}_${side}_") && fname.endsWith(".jpg")
}
val count = (existingFiles?.size ?: 0) + 1
if (!cowFolder.exists()) {
cowFolder.mkdirs()
}
val filename = "${name}_${side}_${count}.jpg"
val file = File(filesDir, filename)
val file = File(cowFolder, filename)
val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build()
@ -113,8 +169,8 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e("MainActivity", "Photo capture failed: ${exc.message}", exc)
Toast.makeText(baseContext, "Photo capture failed", Toast.LENGTH_SHORT).show()
Log.e("CameraProcessor", "Photo capture failed: ${exc.message}", exc)
Toast.makeText(baseContext, StringProvider.getString("toast_capture_failed"), Toast.LENGTH_SHORT).show()
isPhotoTaken = false
}
@ -134,16 +190,30 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
}
}
// Save to public gallery
saveToGallery(file)
val retakePath = intent.getStringExtra("RETAKE_IMAGE_PATH")
if (!retakePath.isNullOrEmpty()) {
val oldFile = File(retakePath)
if (oldFile.exists()) {
oldFile.delete()
}
}
val msg = "Saved as $filename"
val msg = "${StringProvider.getString("toast_saved_as")} $filename"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
// Navigate to FullScreenImageActivity
val intent = Intent(this@CameraProcessor, FullScreenImageActivity::class.java)
intent.putExtra("IMAGE_PATH", file.absolutePath)
intent.putExtra("ALLOW_RETAKE", true)
intent.putExtra("COW_NAME", cowName)
intent.putExtra("ORIENTATION", orientation)
intent.putExtra("SILHOUETTE_ID", silhouetteId)
startActivity(intent)
finish()
} catch (e: Exception) {
Log.e("MainActivity", "Error saving image", e)
Toast.makeText(baseContext, "Error saving image", Toast.LENGTH_SHORT).show()
Log.e("CameraProcessor", "Error saving image", e)
Toast.makeText(baseContext, StringProvider.getString("toast_error_saving_image"), Toast.LENGTH_SHORT).show()
isPhotoTaken = false
}
}
@ -152,38 +222,7 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
}
private fun saveToGallery(file: File) {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, file.name)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/AnimalRating")
put(MediaStore.Images.Media.IS_PENDING, 1)
}
}
val collection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
try {
val uri = contentResolver.insert(collection, values)
uri?.let {
contentResolver.openOutputStream(it)?.use { out ->
FileInputStream(file).use { input ->
input.copyTo(out)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
contentResolver.update(it, values, null, null)
}
}
} catch (e: Exception) {
Log.e("CameraProcessor", "Error saving to gallery", e)
}
// Removed
}
private fun loadSavedMask() {
@ -195,17 +234,50 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
try {
val savedBitmap = BitmapFactory.decodeFile(file.absolutePath)
// Apply green color filter for visualization (on original size)
val greenMask = applyGreenColor(savedBitmap)
savedMaskOverlay.setImageBitmap(greenMask)
savedMaskOverlay.alpha = 0.5f
if (savedBitmap != null) {
// Apply green color filter for visualization
if (isMaskDisplayEnabled) {
val greenMask = applyGreenColor(savedBitmap)
// Scale to 640x480 (or flipped for portrait) for comparison
val isPortrait = (side == "front" || side == "back")
val width = if (isPortrait) 480 else 640
val height = if (isPortrait) 640 else 480
// Calculate scale to match FIT_CENTER logic of SilhouetteOverlay
val viewW = savedMaskOverlay.width.toFloat()
val viewH = savedMaskOverlay.height.toFloat()
savedMaskBitmap = Bitmap.createScaledBitmap(savedBitmap, width, height, true)
if (viewW > 0 && viewH > 0) {
val bmpW = greenMask.width.toFloat()
val bmpH = greenMask.height.toFloat()
val scale = kotlin.math.min(viewW / bmpW, viewH / bmpH)
val scaledW = (bmpW * scale).toInt()
val scaledH = (bmpH * scale).toInt()
if (scaledW > 0 && scaledH > 0) {
val scaledBitmap = Bitmap.createScaledBitmap(greenMask, scaledW, scaledH, true)
savedMaskOverlay.setImageBitmap(scaledBitmap)
savedMaskOverlay.scaleType = ImageView.ScaleType.FIT_CENTER // Ensure it centers
}
} else {
// Fallback if view size not ready yet, though post() should handle it
savedMaskOverlay.setImageBitmap(greenMask)
}
savedMaskOverlay.alpha = 0.5f
} else {
savedMaskOverlay.setImageDrawable(null)
}
// Prepare mask for analysis (640x480 target)
// We should ideally match the visible part of the preview
// For simplicity, keeping original scaling logic for analysis as it might depend on full frame
// However, if the overlay is FIT_CENTER, the analysis should likely respect that aspect ratio too
// But for now, let's just fix the visual overlay as requested.
val isPortrait = (side == "front" || side == "back")
val width = if (isPortrait) 480 else 640
val height = if (isPortrait) 640 else 480
savedMaskBitmap = Bitmap.createScaledBitmap(savedBitmap, width, height, true)
}
} catch (e: Exception) {
Log.e("CameraProcessor", "Error loading saved mask", e)
@ -269,6 +341,7 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
}, ContextCompat.getMainExecutor(this))
}
@ExperimentalGetImage
override fun onFrame(imageProxy: ImageProxy) {
if (isPhotoTaken) {
imageProxy.close()
@ -282,10 +355,14 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
runOnUiThread {
if (result.mask != null) {
currentMask = result.mask
segmentationOverlay.setImageBitmap(result.mask)
if (isMaskDisplayEnabled) {
segmentationOverlay.setImageBitmap(result.mask)
} else {
segmentationOverlay.setImageDrawable(null)
}
}
}
if (result.isMatch) {
if (isAutoCapture && result.isMatch) {
takePhoto()
}
}

View File

@ -10,7 +10,6 @@ import android.view.View
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.GridLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.RadioButton
@ -34,51 +33,21 @@ class CowSelectionActivity : AppCompatActivity() {
private var currentCowName: String? = null
private lateinit var imagesContainer: LinearLayout
private val storagePermissionCode = 101
private val orientationViews = mutableMapOf<String, View>()
private val initialImagePaths = mutableSetOf<String>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cow_selection)
// Initialize StringProvider
StringProvider.initialize(this)
setupUIStrings()
// Set UI text from StringProvider
findViewById<TextView>(R.id.tvToolbarTitle)?.text = StringProvider.getString("title_cow_selection")
findViewById<TextView>(R.id.tvAddCowDetails)?.text = StringProvider.getString("title_add_cow_details")
findViewById<TextInputLayout>(R.id.tilSpecies)?.hint = StringProvider.getString("hint_species")
findViewById<TextInputLayout>(R.id.tilBreed)?.hint = StringProvider.getString("hint_breed")
findViewById<TextInputLayout>(R.id.tilAge)?.hint = StringProvider.getString("hint_age")
findViewById<TextInputLayout>(R.id.tilMilk)?.hint = StringProvider.getString("hint_milk_yield")
findViewById<TextInputLayout>(R.id.tilCalving)?.hint = StringProvider.getString("hint_calving_number")
findViewById<TextInputLayout>(R.id.tilDescription)?.hint = StringProvider.getString("hint_description")
findViewById<TextView>(R.id.tvReproductiveStatus)?.text = StringProvider.getString("label_reproductive_status")
findViewById<RadioButton>(R.id.rbPregnant)?.text = StringProvider.getString("radio_pregnant")
findViewById<RadioButton>(R.id.rbCalved)?.text = StringProvider.getString("radio_calved")
findViewById<RadioButton>(R.id.rbNone)?.text = StringProvider.getString("radio_none")
findViewById<TextView>(R.id.tvUploadPhotos)?.text = StringProvider.getString("label_upload_photos")
findViewById<Button>(R.id.btnNewCow)?.text = StringProvider.getString("btn_save_profile")
findViewById<Button>(R.id.btnCancel)?.text = StringProvider.getString("btn_cancel")
findViewById<TextView>(R.id.tvFrontView)?.text = StringProvider.getString("text_front_view")
findViewById<TextView>(R.id.tvRearView)?.text = StringProvider.getString("text_rear_view")
findViewById<TextView>(R.id.tvLeftSide)?.text = StringProvider.getString("text_left_side")
findViewById<TextView>(R.id.tvRightSide)?.text = StringProvider.getString("text_right_side")
findViewById<TextView>(R.id.tvAngleView)?.text = StringProvider.getString("text_angle_view")
// Setup Toolbar Navigation Click (Exit instead of Back)
val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
// NOTE: The navigation icon is set in XML (@drawable/ic_back_arrow).
// We handle the click here.
toolbar.setNavigationOnClickListener {
// Exit behavior: Finish this activity.
// If "Exit" means close app, use finishAffinity() but user likely means exit this screen.
// Given "except home activity", finish() goes back to home or previous screen.
finish()
}
@ -87,20 +56,24 @@ class CowSelectionActivity : AppCompatActivity() {
imagesContainer = findViewById(R.id.currentCowImagesContainer)
// Restore cow name if available, or generate new if requested
currentCowName = savedInstanceState?.getString("COW_NAME") ?: intent.getStringExtra("COW_NAME")
if (currentCowName == null) {
generateNewCowName()
}
// Try to load existing data if we are editing an existing cow
loadInitialImages()
if (intent.hasExtra("COW_NAME")) {
loadCowDetails(currentCowName!!)
}
updateCowNameDisplay()
refreshCowImages()
orientationViews["left"] = findViewById(R.id.btnLeft)
orientationViews["right"] = findViewById(R.id.btnRight)
orientationViews["angle"] = findViewById(R.id.btnTop)
orientationViews["front"] = findViewById(R.id.btnFront)
orientationViews["back"] = findViewById(R.id.btnBack)
orientationViews["left_angle"] = findViewById(R.id.btnLeftAngle)
orientationViews["right_angle"] = findViewById(R.id.btnRightAngle)
findViewById<Button>(R.id.btnNewCow).setOnClickListener {
if (checkStoragePermissions()) {
@ -111,30 +84,70 @@ class CowSelectionActivity : AppCompatActivity() {
}
findViewById<Button>(R.id.btnCancel).setOnClickListener {
deleteSessionImages()
finish()
}
}
val buttons = mapOf(
R.id.btnLeft to Pair(R.drawable.left, "left"),
R.id.btnRight to Pair(R.drawable.right, "right"),
R.id.btnTop to Pair(R.drawable.angle, "angle"),
R.id.btnFront to Pair(R.drawable.front, "front"),
R.id.btnBack to Pair(R.drawable.back, "back")
)
buttons.forEach { (btnId, pair) ->
val (drawableId, orientation) = pair
// Cast to View generic, as it is now a LinearLayout (clickable) in XML
findViewById<View>(btnId).setOnClickListener {
val intent = Intent(this, CameraProcessor::class.java)
intent.putExtra("SILHOUETTE_ID", drawableId)
intent.putExtra("COW_NAME", currentCowName)
intent.putExtra("ORIENTATION", orientation)
startActivity(intent)
private fun loadInitialImages() {
val name = currentCowName ?: return
val cowFolder = StorageUtils.getCowImageFolder(name)
if (cowFolder.exists()) {
cowFolder.listFiles()?.forEach { file ->
if (file.isFile) {
initialImagePaths.add(file.absolutePath)
}
}
}
}
private fun deleteSessionImages() {
val name = currentCowName ?: return
val cowFolder = StorageUtils.getCowImageFolder(name)
if (cowFolder.exists()) {
cowFolder.listFiles()?.forEach { file ->
if (file.isFile && !initialImagePaths.contains(file.absolutePath)) {
file.delete()
}
}
val remaining = cowFolder.listFiles()
if (remaining == null || remaining.isEmpty()) {
cowFolder.delete()
}
}
}
private fun setupUIStrings() {
findViewById<TextView>(R.id.tvToolbarTitle).text = StringProvider.getString("title_cow_selection")
findViewById<TextView>(R.id.tvAddCowDetails).text = StringProvider.getString("title_add_cow_details")
// Using hint_ keys for the new labels as they contain the appropriate text (e.g. "Species", "Breed")
findViewById<TextView>(R.id.tvLabelSpecies).text = StringProvider.getString("hint_species")
findViewById<TextView>(R.id.tvLabelBreed).text = StringProvider.getString("hint_breed")
findViewById<TextView>(R.id.tvLabelAge).text = StringProvider.getString("hint_age")
findViewById<TextView>(R.id.tvLabelMilk).text = StringProvider.getString("hint_milk_yield")
findViewById<TextView>(R.id.tvLabelCalving).text = StringProvider.getString("hint_calving_number")
findViewById<TextView>(R.id.tvLabelDescription).text = StringProvider.getString("hint_description")
findViewById<TextInputLayout>(R.id.tilSpecies).hint = null
findViewById<TextInputLayout>(R.id.tilBreed).hint = null
findViewById<TextInputLayout>(R.id.tilAge).hint = null
findViewById<TextInputLayout>(R.id.tilMilk).hint = null
findViewById<TextInputLayout>(R.id.tilCalving).hint = null
findViewById<TextInputLayout>(R.id.tilDescription).hint = null
findViewById<TextView>(R.id.tvReproductiveStatus).text = StringProvider.getString("label_reproductive_status")
findViewById<RadioButton>(R.id.rbPregnant).text = StringProvider.getString("radio_pregnant")
findViewById<RadioButton>(R.id.rbCalved).text = StringProvider.getString("radio_calved")
findViewById<RadioButton>(R.id.rbNone).text = StringProvider.getString("radio_none")
findViewById<TextView>(R.id.tvUploadPhotos).text = StringProvider.getString("label_upload_photos")
findViewById<Button>(R.id.btnNewCow).text = StringProvider.getString("btn_save_profile")
findViewById<Button>(R.id.btnCancel).text = StringProvider.getString("btn_cancel")
}
private fun checkStoragePermissions(): Boolean {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
android.os.Environment.isExternalStorageManager()
@ -166,15 +179,9 @@ class CowSelectionActivity : AppCompatActivity() {
}
private fun saveProfile() {
// Get displayed values
val speciesDisplay = findViewById<AutoCompleteTextView>(R.id.spinnerSpecies).text.toString()
val breedDisplay = findViewById<AutoCompleteTextView>(R.id.spinnerBreed).text.toString()
// Convert to English for storage if needed (using reverse lookup or StringProvider helper)
// Assuming keys like "species_cow" map to "Cow" in English and something else in Hindi
// We try to find the key for the displayed value, then get the English value for that key.
// If not found (e.g. custom input), fallback to displayed value.
val speciesKey = StringProvider.getKeyForValue(speciesDisplay)
val species = if (speciesKey != null) StringProvider.getStringEnglish(speciesKey) else speciesDisplay
@ -198,7 +205,8 @@ class CowSelectionActivity : AppCompatActivity() {
val csvHeader = "CowID,Species,Breed,Age,MilkYield,CalvingNumber,ReproductiveStatus,Description\n"
val csvRow = "$currentCowName,$species,$breed,$ageInput,$milkInput,$calvingInput,$reproductiveStatus,$descriptionInput\n"
val csvFile = File(filesDir, "cow_profiles.csv")
val docsFolder = StorageUtils.getDocumentsFolder()
val csvFile = File(docsFolder, "cow_profiles.csv")
try {
val fileExists = csvFile.exists()
@ -208,7 +216,6 @@ class CowSelectionActivity : AppCompatActivity() {
lines.add(csvHeader.trim())
}
// Check if we are updating
val existingIndex = lines.indexOfFirst { it.startsWith("$currentCowName,") }
if (existingIndex != -1) {
@ -223,9 +230,6 @@ class CowSelectionActivity : AppCompatActivity() {
}
}
// Also export to public storage if needed
saveCsvToPublicStorage(lines)
Toast.makeText(this, StringProvider.getString("toast_profile_saved"), Toast.LENGTH_SHORT).show()
finish()
} catch (e: Exception) {
@ -234,40 +238,16 @@ class CowSelectionActivity : AppCompatActivity() {
}
}
private fun saveCsvToPublicStorage(lines: List<String>) {
// Saving to public Documents folder
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
if (android.os.Environment.isExternalStorageManager()) {
val publicFile = File(android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOCUMENTS), "AnimalRating_Profiles.csv")
FileWriter(publicFile).use { writer ->
lines.forEach { line ->
writer.write(line + "\n")
}
}
}
} else {
val publicFile = File(android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOCUMENTS), "AnimalRating_Profiles.csv")
FileWriter(publicFile).use { writer ->
lines.forEach { line ->
writer.write(line + "\n")
}
}
}
}
private fun loadCowDetails(cowId: String) {
val csvFile = File(filesDir, "cow_profiles.csv")
val docsFolder = StorageUtils.getDocumentsFolder()
val csvFile = File(docsFolder, "cow_profiles.csv")
if (!csvFile.exists()) return
try {
val lines = csvFile.readLines()
// Removed unused header variable
val record = lines.find { it.startsWith("$cowId,") }?.split(",") ?: return
// Simple mapping based on known order
// CowID,Species,Breed,Age,MilkYield,CalvingNumber,ReproductiveStatus,Description
if (record.size >= 8) {
// Data is stored in English. Find key for English value, then get display string for that key.
val storedSpecies = record[1]
val speciesKey = StringProvider.getKeyForEnglishValue(storedSpecies)
val displaySpecies = if (speciesKey != null) StringProvider.getString(speciesKey) else storedSpecies
@ -283,24 +263,13 @@ class CowSelectionActivity : AppCompatActivity() {
findViewById<TextInputLayout>(R.id.tilCalving).editText?.setText(record[5])
val storedStatus = record[6]
// Find which key matches this English value
val statusKey = StringProvider.getKeyForEnglishValue(storedStatus)
// We need to check radio buttons.
// Radio buttons text is set from StringProvider in onCreate.
// So we need to find which RadioButton corresponds to 'statusKey'.
// "radio_pregnant" -> R.id.rbPregnant
// "radio_calved" -> R.id.rbCalved
// "radio_none" -> R.id.rbNone
when(statusKey) {
"radio_pregnant" -> findViewById<RadioButton>(R.id.rbPregnant).isChecked = true
"radio_calved" -> findViewById<RadioButton>(R.id.rbCalved).isChecked = true
"radio_none" -> findViewById<RadioButton>(R.id.rbNone).isChecked = true
// Fallback if stored directly as text or unknown key
else -> {
// If stored directly as English "Pregnant" etc.
when (storedStatus) {
"Pregnant" -> findViewById<RadioButton>(R.id.rbPregnant).isChecked = true
"Calved" -> findViewById<RadioButton>(R.id.rbCalved).isChecked = true
@ -348,7 +317,9 @@ class CowSelectionActivity : AppCompatActivity() {
"right" to R.drawable.right,
"angle" to R.drawable.angle,
"front" to R.drawable.front,
"back" to R.drawable.back
"back" to R.drawable.back,
"leftangle" to R.drawable.leftangle,
"rightangle" to R.drawable.rightangle
)
orientationResources.forEach { (orientation, resId) ->
@ -379,10 +350,8 @@ class CowSelectionActivity : AppCompatActivity() {
for (i in pixels.indices) {
val alpha = (pixels[i] shr 24) and 0xFF
if (alpha > 0) {
// Original was visible -> make transparent
pixels[i] = Color.TRANSPARENT
} else {
// Original was transparent -> make black (opaque)
pixels[i] = Color.BLACK
}
}
@ -394,87 +363,153 @@ class CowSelectionActivity : AppCompatActivity() {
currentCowName = "cow_${sdf.format(Date())}"
}
private fun updateCowNameDisplay() {
// Cow Name is now in Toolbar title if needed, but we hide title.
// findViewById<TextView>(R.id.tvCowName).text = "Current Cow: $currentCowName"
}
private fun refreshCowImages() {
imagesContainer.removeAllViews()
val name = currentCowName ?: return
val files = filesDir.listFiles { _, fname -> fname.startsWith("${name}_") && fname.endsWith(".jpg") }
if (files.isNullOrEmpty()) return
val orientations = mapOf(
"left" to Pair(R.drawable.left, R.id.btnLeft),
"right" to Pair(R.drawable.right, R.id.btnRight),
"angle" to Pair(R.drawable.angle, R.id.btnTop),
"front" to Pair(R.drawable.front, R.id.btnFront),
"back" to Pair(R.drawable.back, R.id.btnBack),
"leftangle" to Pair(R.drawable.leftangle, R.id.btnLeftAngle),
"rightangle" to Pair(R.drawable.rightangle, R.id.btnRightAngle)
)
// Group files by orientation
val filesByOrientation = files.groupBy { file ->
val parts = file.name.split("_")
if (parts.size >= 3) parts[2] else "unknown"
}
val cowImagesFolder = StorageUtils.getCowImageFolder(name)
filesByOrientation.forEach { (orientation, orientationFiles) ->
addOrientationSection(orientation, orientationFiles)
}
}
orientations.forEach { (orientation, pair) ->
val (drawableId, viewId) = pair
private fun addOrientationSection(orientationStr: String, files: List<File>) {
// Orientation Header
val orientationHeader = TextView(this).apply {
val key = when(orientationStr.lowercase(Locale.getDefault())) {
val files = if (cowImagesFolder.exists()) {
cowImagesFolder.listFiles { _, fname -> fname.startsWith("${name}_${orientation}_") && fname.endsWith(".jpg") }
} else {
null
}
val latestFile = files?.maxByOrNull { it.lastModified() }
val container = findViewById<LinearLayout>(viewId)
container.removeAllViews()
val key = when(orientation) {
"front" -> "text_front_view"
"back" -> "text_rear_view"
"left" -> "text_left_side"
"right" -> "text_right_side"
"angle" -> "text_angle_view"
"leftangle" -> "text_left_angle"
"rightangle" -> "text_right_angle"
else -> ""
}
val label = if (key.isNotEmpty()) StringProvider.getString(key) else orientationStr.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
text = label
val label = if (key.isNotEmpty()) StringProvider.getString(key) else orientation
textSize = 16f
setTypeface(null, android.graphics.Typeface.BOLD)
setPadding(16, 8, 0, 8)
}
imagesContainer.addView(orientationHeader)
if (latestFile != null && latestFile.exists()) {
val frameLayout = android.widget.FrameLayout(this)
frameLayout.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
// Grid for thumbnails
val gridLayout = GridLayout(this).apply {
columnCount = 3
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(16, 0, 16, 16)
}
}
val imageView = ImageView(this)
imageView.layoutParams = android.widget.FrameLayout.LayoutParams(
android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
android.widget.FrameLayout.LayoutParams.MATCH_PARENT
)
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
val bitmap = BitmapFactory.decodeFile(latestFile.absolutePath)
imageView.setImageBitmap(bitmap)
// Images for this orientation
files.forEach { file ->
val thumbnailView = layoutInflater.inflate(R.layout.item_image_thumbnail, gridLayout, false)
val imageView = thumbnailView.findViewById<ImageView>(R.id.ivThumbnail)
val deleteButton = thumbnailView.findViewById<View>(R.id.btnDelete)
imageView.setImageBitmap(BitmapFactory.decodeFile(file.absolutePath))
imageView.setOnClickListener {
val intent = Intent(this@CowSelectionActivity, FullScreenImageActivity::class.java)
intent.putExtra("IMAGE_PATH", file.absolutePath)
startActivity(intent)
}
deleteButton.setOnClickListener {
if (file.delete()) {
Toast.makeText(this@CowSelectionActivity, StringProvider.getString("toast_image_deleted"), Toast.LENGTH_SHORT).show()
refreshCowImages()
} else {
Toast.makeText(this@CowSelectionActivity, StringProvider.getString("toast_error_deleting_image"), Toast.LENGTH_SHORT).show()
val deleteBtn = ImageView(this)
val btnSize = (24 * resources.displayMetrics.density).toInt()
val btnParams = android.widget.FrameLayout.LayoutParams(btnSize, btnSize)
btnParams.gravity = android.view.Gravity.TOP or android.view.Gravity.END
val margin = (4 * resources.displayMetrics.density).toInt()
btnParams.setMargins(margin, margin, margin, margin)
deleteBtn.layoutParams = btnParams
deleteBtn.setImageResource(android.R.drawable.ic_menu_close_clear_cancel)
deleteBtn.setColorFilter(Color.RED)
deleteBtn.setBackgroundColor(Color.parseColor("#80FFFFFF"))
deleteBtn.isClickable = true
deleteBtn.setOnClickListener {
if (latestFile.delete()) {
val remainingFiles = cowImagesFolder.listFiles()
if (remainingFiles == null || remainingFiles.isEmpty()) {
cowImagesFolder.delete()
}
refreshCowImages()
}
}
}
gridLayout.addView(thumbnailView)
val labelView = TextView(this)
labelView.text = label
labelView.textSize = 12f
labelView.setTextColor(Color.WHITE)
labelView.setShadowLayer(3f, 0f, 0f, Color.BLACK)
val labelParams = android.widget.FrameLayout.LayoutParams(
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT,
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT
)
labelParams.gravity = android.view.Gravity.BOTTOM or android.view.Gravity.CENTER_HORIZONTAL
labelParams.bottomMargin = (4 * resources.displayMetrics.density).toInt()
labelView.layoutParams = labelParams
frameLayout.addView(imageView)
frameLayout.addView(labelView)
frameLayout.addView(deleteBtn)
container.addView(frameLayout)
imageView.setOnClickListener {
val intent = Intent(this, FullScreenImageActivity::class.java)
intent.putExtra("IMAGE_PATH", latestFile.absolutePath)
intent.putExtra("ALLOW_RETAKE", true)
intent.putExtra("COW_NAME", currentCowName)
intent.putExtra("ORIENTATION", orientation)
intent.putExtra("SILHOUETTE_ID", drawableId)
startActivity(intent)
}
container.setOnClickListener(null)
container.isClickable = false
} else {
val iconView = ImageView(this)
val params = LinearLayout.LayoutParams(
(24 * resources.displayMetrics.density).toInt(),
(24 * resources.displayMetrics.density).toInt()
)
params.gravity = android.view.Gravity.CENTER_HORIZONTAL
iconView.layoutParams = params
iconView.setImageResource(android.R.drawable.ic_menu_camera)
iconView.setColorFilter(Color.parseColor("#5D4037"))
val textView = TextView(this)
val textParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
textParams.gravity = android.view.Gravity.CENTER_HORIZONTAL
textParams.topMargin = (4 * resources.displayMetrics.density).toInt()
textView.layoutParams = textParams
textView.text = label
textView.textSize = 12f
textView.setTextColor(Color.parseColor("#5D4037"))
container.addView(iconView)
container.addView(textView)
container.setOnClickListener {
val intent = Intent(this, CameraProcessor::class.java)
intent.putExtra("SILHOUETTE_ID", drawableId)
intent.putExtra("COW_NAME", currentCowName)
intent.putExtra("ORIENTATION", orientation)
startActivity(intent)
}
container.isClickable = true
}
}
imagesContainer.addView(gridLayout)
}
override fun onSaveInstanceState(outState: Bundle) {

View File

@ -93,11 +93,19 @@ class FrameProcessor {
val rawBitmap = Bitmap.createBitmap(colors, fullWidth, fullHeight, Bitmap.Config.ARGB_8888)
// Rotate if needed
// Rotate and Scale if needed
bitmapMask = if (isPortrait) {
try {
val matrix = Matrix()
// Rotate 90 degrees
matrix.postRotate(90f)
// Assuming previous scaling logic might still be relevant if masks are consistently off
// But generally, we trust the geometry.
// If scaling up is happening unexpectedly, one could use:
// matrix.postScale(0.9f, 0.9f)
// For now, stick to rotation unless requested otherwise.
Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true)
} catch (e: Exception) {
Log.e("FrameProcessor", "Error rotating mask", e)
@ -127,7 +135,17 @@ class FrameProcessor {
}
if (comparisonMask != null) {
isMatch = when (algorithm) {
// Map display strings to internal algorithm keys/logic
val algoKey = when(algorithm) {
StringProvider.getString("algo_euclidean") -> HomeActivity.ALGORITHM_EUCLIDEAN
StringProvider.getString("algo_jaccard") -> HomeActivity.ALGORITHM_JACCARD
else -> {
if (algorithm == HomeActivity.ALGORITHM_EUCLIDEAN || algorithm == HomeActivity.ALGORITHM_JACCARD) algorithm
else HomeActivity.ALGORITHM_HAMMING
}
}
isMatch = when (algoKey) {
HomeActivity.ALGORITHM_EUCLIDEAN -> calculateEuclideanDistance(savedMask, comparisonMask, thresholdPercent)
HomeActivity.ALGORITHM_JACCARD -> calculateJaccardSimilarity(savedMask, comparisonMask, thresholdPercent)
else -> calculateHammingDistance(savedMask, comparisonMask, thresholdPercent)
@ -229,19 +247,13 @@ class FrameProcessor {
var union = 0
for (i in pixels1.indices) {
// mask1 is Saved Mask (Inverse: Subject is Transparent/0, Background is Black/255)
// NOTE: Wait, the saved mask was inverted originally?
// Let's stick to generic "isSet" logic.
// In previous turns, I assumed mask1 was "Inverse" and mask2 was "Normal".
// But for general algorithms, let's normalize.
// In CameraProcessor, we load the saved mask. It is likely the inverted one.
// Let's assume:
// Saved Mask (mask1): Alpha < 128 means subject (because it's inverted)
// Current Mask (mask2): Alpha > 0 means subject
// mask1 is Saved Mask (Likely Inverted in StorageUtils: Subject is Transparent/0, Background is Black/255)
// But check how it's loaded. In CameraProcessor: loadSavedMask() -> decodeFile().
// If we assume saved mask is inverted (subject transparent), then alpha < 128 is subject.
val alpha1 = (pixels1[i] ushr 24) and 0xFF
val isSubject1 = alpha1 < 128
// mask2 is Live Mask (Subject is Magenta/180 alpha)
val alpha2 = (pixels2[i] ushr 24) and 0xFF
val isSubject2 = alpha2 > 0

View File

@ -1,8 +1,11 @@
package com.example.animalrating
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import java.io.File
@ -13,7 +16,14 @@ class FullScreenImageActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_full_screen_image)
StringProvider.initialize(this)
val imagePath = intent.getStringExtra("IMAGE_PATH")
val allowRetake = intent.getBooleanExtra("ALLOW_RETAKE", false)
val cowName = intent.getStringExtra("COW_NAME")
val orientation = intent.getStringExtra("ORIENTATION")
val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0)
if (imagePath != null) {
val file = File(imagePath)
if (file.exists()) {
@ -22,8 +32,31 @@ class FullScreenImageActivity : AppCompatActivity() {
}
}
findViewById<Button>(R.id.btnBack).setOnClickListener {
val btnRetake = findViewById<Button>(R.id.btnRetake)
if (allowRetake && imagePath != null) {
btnRetake.visibility = View.VISIBLE
btnRetake.text = "Retake Photo"
btnRetake.setOnClickListener {
// Launch camera to retake. Pass the current image path so CameraProcessor can overwrite/delete it on success.
val intent = Intent(this, CameraProcessor::class.java)
intent.putExtra("SILHOUETTE_ID", silhouetteId)
intent.putExtra("COW_NAME", cowName)
intent.putExtra("ORIENTATION", orientation)
intent.putExtra("RETAKE_IMAGE_PATH", imagePath) // Pass image path to replace
startActivity(intent)
finish()
}
} else {
btnRetake.visibility = View.GONE
}
val btnBack = findViewById<ImageButton>(R.id.btnBack)
btnBack.contentDescription = StringProvider.getString("content_desc_back")
btnBack.setOnClickListener {
finish()
}
findViewById<ImageView>(R.id.fullScreenImageView).contentDescription = StringProvider.getString("content_desc_full_screen_image")
}
}

View File

@ -4,14 +4,17 @@ import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.BitmapFactory
import android.os.Bundle
import android.view.Gravity
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import com.google.android.material.button.MaterialButton
import com.google.android.material.floatingactionbutton.FloatingActionButton
import java.io.File
import java.io.FileWriter
import java.util.Locale
class GalleryActivity : AppCompatActivity() {
@ -22,17 +25,14 @@ class GalleryActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gallery)
// Initialize StringProvider
StringProvider.initialize(this)
// Setup Back Button
val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false)
// Set Toolbar title
findViewById<TextView>(R.id.tvToolbarTitle)?.text = StringProvider.getString("title_gallery")
toolbar.setNavigationOnClickListener {
@ -52,15 +52,14 @@ class GalleryActivity : AppCompatActivity() {
private fun refreshGallery() {
container.removeAllViews()
val files = filesDir.listFiles { _, name -> name.startsWith("cow_") && name.endsWith(".jpg") }
val imagesBaseFolder = StorageUtils.getImagesBaseFolder()
val cowFolders = imagesBaseFolder.listFiles { file -> file.isDirectory } ?: emptyArray()
// Group files by cow name
val groupedFiles = files?.groupBy { file ->
val parts = file.name.split("_")
if (parts.size >= 2) "${parts[0]}_${parts[1]}" else "unknown"
} ?: emptyMap()
val cowNamesFromFolders = cowFolders.map { it.name }
val docsFolder = StorageUtils.getDocumentsFolder()
val csvFile = File(docsFolder, "cow_profiles.csv")
val csvFile = File(filesDir, "cow_profiles.csv")
val cowDetails = if (csvFile.exists()) {
csvFile.readLines().associate { line ->
val parts = line.split(",")
@ -70,21 +69,51 @@ class GalleryActivity : AppCompatActivity() {
emptyMap()
}
groupedFiles.forEach { (cowName, cowFiles) ->
val allCowNames = (cowDetails.keys + cowNamesFromFolders).filter { it.isNotEmpty() && it != "CowID" }.distinct()
allCowNames.forEach { cowName ->
val details = cowDetails[cowName] ?: emptyList()
val cowImageFolder = StorageUtils.getCowImageFolder(cowName)
val cowFiles = cowImageFolder.listFiles { _, name -> name.endsWith(".jpg") }?.toList() ?: emptyList()
addCowSection(cowName, cowFiles, details)
}
}
private fun addCowSection(cowName: String, cowFiles: List<File>, details: List<String>) {
// Cow Name Header and Retake Button
val headerLayout = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
// Main Card
val card = CardView(this).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 32, 0, 16)
setMargins(0, 0, 0, 24)
}
radius = 16 * resources.displayMetrics.density
cardElevation = 2 * resources.displayMetrics.density
setCardBackgroundColor(android.graphics.Color.WHITE)
}
// Horizontal Container (3:1 split)
val horizontalContainer = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
weightSum = 4f
setPadding(24, 24, 24, 24)
}
// Left Layout (Info) - Weight 3
val leftLayout = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
3f
).apply {
marginEnd = 16
}
}
@ -93,14 +122,74 @@ class GalleryActivity : AppCompatActivity() {
textSize = 20f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(android.graphics.Color.parseColor("#3E2723"))
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
bottomMargin = 8
}
}
leftLayout.addView(nameView)
if (details.size >= 7) {
// Translate values for display
val storedSpecies = details.getOrElse(1) { "-" }
val speciesKey = StringProvider.getKeyForEnglishValue(storedSpecies)
val displaySpecies = if (speciesKey != null) StringProvider.getString(speciesKey) else storedSpecies
val storedBreed = details.getOrElse(2) { "-" }
val breedKey = StringProvider.getKeyForEnglishValue(storedBreed)
val displayBreed = if (breedKey != null) StringProvider.getString(breedKey) else storedBreed
val storedStatus = details.getOrElse(6) { "-" }
val statusKey = StringProvider.getKeyForEnglishValue(storedStatus)
val displayStatus = if (statusKey != null) StringProvider.getString(statusKey) else storedStatus
val infoText = StringBuilder()
infoText.append("${StringProvider.getString("label_species")} $displaySpecies ")
infoText.append("${StringProvider.getString("label_breed")} $displayBreed\n")
infoText.append("${StringProvider.getString("label_age")} ${details.getOrElse(3) { "-" }} ${StringProvider.getString("unit_years")} ")
infoText.append("${StringProvider.getString("label_milk_yield")} ${details.getOrElse(4) { "-" }} ${StringProvider.getString("unit_liters")}\n")
infoText.append("${StringProvider.getString("label_calving_no")} ${details.getOrElse(5) { "-" }} ")
infoText.append("${StringProvider.getString("label_status")} $displayStatus")
val detailsView = TextView(this).apply {
text = infoText.toString()
textSize = 14f
setTextColor(android.graphics.Color.parseColor("#5D4037"))
setLineSpacing(10f, 1f)
}
leftLayout.addView(detailsView)
} else {
val detailsView = TextView(this).apply {
text = "No details available"
textSize = 14f
setTextColor(android.graphics.Color.parseColor("#5D4037"))
}
leftLayout.addView(detailsView)
}
horizontalContainer.addView(leftLayout)
// Right Layout (Buttons) - Weight 1
val rightLayout = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
)
gravity = Gravity.CENTER_VERTICAL
}
val retakeButton = MaterialButton(this).apply {
val buttonParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
(48 * resources.displayMetrics.density).toInt()
).apply {
bottomMargin = (8 * resources.displayMetrics.density).toInt()
}
val editButton = MaterialButton(this).apply {
text = StringProvider.getString("btn_edit")
textSize = 12f
setTypeface(null, android.graphics.Typeface.BOLD)
@ -109,9 +198,11 @@ class GalleryActivity : AppCompatActivity() {
backgroundTintList = ColorStateList.valueOf(android.graphics.Color.parseColor("#EFEBE9"))
strokeWidth = (1 * resources.displayMetrics.density).toInt()
strokeColor = ColorStateList.valueOf(android.graphics.Color.parseColor("#5D4037"))
layoutParams = buttonParams
insetTop = 0
insetBottom = 0
minHeight = 0
minimumHeight = 0
setPadding(24, 20, 24, 20)
setOnClickListener {
val intent = Intent(this@GalleryActivity, CowSelectionActivity::class.java)
@ -120,132 +211,186 @@ class GalleryActivity : AppCompatActivity() {
}
}
headerLayout.addView(nameView)
headerLayout.addView(retakeButton)
container.addView(headerLayout)
val rateButton = MaterialButton(this).apply {
text = "Rate" // Add string if needed, skipping as per instruction to be concise unless requested
textSize = 12f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(android.graphics.Color.WHITE)
cornerRadius = (12 * resources.displayMetrics.density).toInt()
backgroundTintList = ColorStateList.valueOf(android.graphics.Color.parseColor("#6D4C41"))
layoutParams = buttonParams
insetTop = 0
insetBottom = 0
minHeight = 0
minimumHeight = 0
// Display details if available
// Header: CowID,Species,Breed,Age,MilkYield,CalvingNumber,ReproductiveStatus,Description
if (details.size >= 7) {
val detailsLayout = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setOnClickListener {
val intent = Intent(this@GalleryActivity, RatingActivity::class.java)
intent.putExtra("COW_NAME", cowName)
startActivity(intent)
}
}
val deleteButton = MaterialButton(this).apply {
text = "Delete"
textSize = 12f
setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(android.graphics.Color.WHITE)
cornerRadius = (12 * resources.displayMetrics.density).toInt()
backgroundTintList = ColorStateList.valueOf(android.graphics.Color.RED)
layoutParams = buttonParams
insetTop = 0
insetBottom = 0
minHeight = 0
minimumHeight = 0
setOnClickListener {
deleteCow(cowName)
}
}
rightLayout.addView(editButton)
rightLayout.addView(rateButton)
rightLayout.addView(deleteButton)
horizontalContainer.addView(rightLayout)
card.addView(horizontalContainer)
container.addView(card)
// Orientation Images
if (cowFiles.isNotEmpty()) {
// Instead of separating by orientation, put all images in a grid
val gridLayout = android.widget.GridLayout(this).apply {
columnCount = 3
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, 0, 0, 16)
}
setPadding(24, 24, 24, 24)
background = android.graphics.drawable.GradientDrawable().apply {
setColor(android.graphics.Color.parseColor("#FAFAFA"))
cornerRadius = 16f
setStroke(2, android.graphics.Color.parseColor("#EEEEEE"))
setMargins(16, 0, 16, 16)
}
}
val infoText = StringBuilder()
// Note: Values like "Cow", "Holstein" stored in CSV are currently displayed as is.
// To translate stored values, we'd need a reverse lookup map or similar logic if the CSV stores English keys.
// For now, we translate the labels.
// Sort images or keep them in order. The user asked for 3 in each row.
cowFiles.forEach { file ->
val thumbnailView = layoutInflater.inflate(R.layout.item_image_thumbnail, gridLayout, false)
infoText.append("${StringProvider.getString("label_species")} ${details.getOrElse(1) { "-" }} ")
infoText.append("${StringProvider.getString("label_breed")} ${details.getOrElse(2) { "-" }}\n")
infoText.append("${StringProvider.getString("label_age")} ${details.getOrElse(3) { "-" }} ${StringProvider.getString("unit_years")} ")
infoText.append("${StringProvider.getString("label_milk_yield")} ${details.getOrElse(4) { "-" }} ${StringProvider.getString("unit_liters")}\n")
infoText.append("${StringProvider.getString("label_calving_no")} ${details.getOrElse(5) { "-" }} ")
infoText.append("${StringProvider.getString("label_status")} ${details.getOrElse(6) { "-" }}")
val imageView = thumbnailView.findViewById<ImageView>(R.id.ivThumbnail)
val labelView = thumbnailView.findViewById<TextView>(R.id.tvOrientationLabel)
val deleteButtonSmall = thumbnailView.findViewById<android.view.View>(R.id.btnDelete)
val detailsView = TextView(this).apply {
text = infoText.toString()
textSize = 14f
setTextColor(android.graphics.Color.parseColor("#5D4037"))
setLineSpacing(10f, 1f)
// Optionally set layout params for thumbnailView to ensure 3 per row
val displayMetrics = resources.displayMetrics
val screenWidth = displayMetrics.widthPixels
val itemWidth = (screenWidth - (48 * displayMetrics.density).toInt()) / 3
thumbnailView.layoutParams = android.widget.GridLayout.LayoutParams().apply {
width = itemWidth
height = itemWidth // Square
setMargins(4, 4, 4, 4)
}
// Extract orientation from filename
val parts = file.name.split("_")
var orientation = ""
if (parts.size >= 3) {
// format: cow_..._orientation_timestamp.jpg
// or cow_timestamp_orientation_...
// Let's guess. In CowSelectionActivity: ${name}_${orientation}_...
// name = cow_...
// so filename starts with name
// Let's just try to find a known orientation in the filename
val orientations = listOf("left", "right", "angle", "front", "back", "leftangle", "rightangle")
orientation = orientations.find { file.name.contains("_${it}_") } ?: ""
}
val key = when(orientation) {
"front" -> "text_front_view"
"back" -> "text_rear_view"
"left" -> "text_left_side"
"right" -> "text_right_side"
"angle" -> "text_angle_view"
"leftangle" -> "text_left_angle"
"rightangle" -> "text_right_angle"
else -> ""
}
val label = if (key.isNotEmpty()) StringProvider.getString(key) else orientation
labelView.text = label
imageView.setImageBitmap(BitmapFactory.decodeFile(file.absolutePath))
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
imageView.setOnClickListener {
val intent = Intent(this@GalleryActivity, FullScreenImageActivity::class.java)
intent.putExtra("IMAGE_PATH", file.absolutePath)
startActivity(intent)
}
deleteButtonSmall.setOnClickListener {
if (file.delete()) {
val parentDir = file.parentFile
if (parentDir != null && parentDir.exists()) {
val remaining = parentDir.listFiles()
if (remaining == null || remaining.isEmpty()) {
parentDir.delete()
}
}
Toast.makeText(this@GalleryActivity, StringProvider.getString("toast_image_deleted"), Toast.LENGTH_SHORT).show()
refreshGallery()
} else {
Toast.makeText(this@GalleryActivity, StringProvider.getString("toast_error_deleting_image"), Toast.LENGTH_SHORT).show()
}
}
gridLayout.addView(thumbnailView)
}
container.addView(gridLayout)
detailsLayout.addView(detailsView)
container.addView(detailsLayout)
}
// Group files by orientation (e.g., left, right, etc.)
val filesByOrientation = cowFiles.groupBy { file ->
val parts = file.name.split("_")
if (parts.size >= 3) parts[2] else "unknown"
}
filesByOrientation.forEach { (orientation, orientationFiles) ->
addOrientationSection(orientation, orientationFiles)
}
// Separator
val separator = android.view.View(this).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
2
).apply {
setMargins(0, 16, 0, 16)
val separator = android.view.View(this).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
2
).apply {
setMargins(0, 16, 0, 16)
}
setBackgroundColor(android.graphics.Color.LTGRAY)
}
setBackgroundColor(android.graphics.Color.LTGRAY)
container.addView(separator)
}
container.addView(separator)
}
private fun addOrientationSection(orientationStr: String, files: List<File>) {
// Orientation Header
val orientationHeader = TextView(this).apply {
val key = when(orientationStr.lowercase(Locale.getDefault())) {
"front" -> "text_front_view"
"back" -> "text_rear_view"
"left" -> "text_left_side"
"right" -> "text_right_side"
"angle" -> "text_angle_view"
else -> ""
}
val label = if (key.isNotEmpty()) StringProvider.getString(key) else orientationStr.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
text = label
textSize = 16f
setTypeface(null, android.graphics.Typeface.BOLD)
setPadding(16, 8, 0, 8)
private fun deleteCow(cowName: String) {
// Delete Images
val imageFolder = StorageUtils.getCowImageFolder(cowName)
if (imageFolder.exists()) {
imageFolder.deleteRecursively()
}
container.addView(orientationHeader)
// Grid for thumbnails
val gridLayout = android.widget.GridLayout(this).apply {
columnCount = 3
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
setMargins(16, 0, 16, 16)
val docsFolder = StorageUtils.getDocumentsFolder()
// Delete Profile
val profileFile = File(docsFolder, "cow_profiles.csv")
if (profileFile.exists()) {
val lines = profileFile.readLines()
val newLines = lines.filter { !it.startsWith("$cowName,") }
FileWriter(profileFile).use { writer ->
newLines.forEach { writer.write(it + "\n") }
}
}
// Images for this orientation
files.forEach { file ->
val thumbnailView = layoutInflater.inflate(R.layout.item_image_thumbnail, gridLayout, false)
val imageView = thumbnailView.findViewById<ImageView>(R.id.ivThumbnail)
val deleteButton = thumbnailView.findViewById<android.view.View>(R.id.btnDelete)
imageView.setImageBitmap(BitmapFactory.decodeFile(file.absolutePath))
imageView.setOnClickListener {
val intent = Intent(this@GalleryActivity, FullScreenImageActivity::class.java)
intent.putExtra("IMAGE_PATH", file.absolutePath)
startActivity(intent)
// Delete Ratings
val ratingsFile = File(docsFolder, "cow_ratings.csv")
if (ratingsFile.exists()) {
val lines = ratingsFile.readLines()
val newLines = lines.filter { !it.startsWith("$cowName,") }
FileWriter(ratingsFile).use { writer ->
newLines.forEach { writer.write(it + "\n") }
}
deleteButton.setOnClickListener {
if (file.delete()) {
Toast.makeText(this@GalleryActivity, StringProvider.getString("toast_image_deleted"), Toast.LENGTH_SHORT).show()
refreshGallery()
} else {
Toast.makeText(this@GalleryActivity, StringProvider.getString("toast_error_deleting_image"), Toast.LENGTH_SHORT).show()
}
}
gridLayout.addView(thumbnailView)
}
container.addView(gridLayout)
Toast.makeText(this, "Cow profile deleted", Toast.LENGTH_SHORT).show()
refreshGallery()
}
override fun onResume() {

View File

@ -2,20 +2,27 @@ package com.example.animalrating
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.SeekBar
import android.widget.Spinner
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import com.google.android.material.button.MaterialButton
import com.google.android.material.switchmaterial.SwitchMaterial
class HomeActivity : AppCompatActivity() {
@ -25,8 +32,14 @@ class HomeActivity : AppCompatActivity() {
const val ALGORITHM_JACCARD = "Jaccard Similarity"
private const val PERMISSION_REQUEST_CODE = 101
private const val PREF_COW_ILLUSTRATION_INDEX = "COW_ILLUSTRATION_INDEX"
const val PREF_AUTO_CAPTURE = "AUTO_CAPTURE_ENABLED"
const val PREF_DEBUG_ENABLE = "AUTO_DEBUG_ENABLED"
const val PREF_MASK_DISPLAY = "MASK_DISPLAY_ENABLED"
var IS_RELEASE_BUILD = true // Set to true for release
}
private val internalAlgorithms = listOf(ALGORITHM_HAMMING, ALGORITHM_EUCLIDEAN, ALGORITHM_JACCARD)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
@ -44,23 +57,27 @@ class HomeActivity : AppCompatActivity() {
permissions.add(android.Manifest.permission.CAMERA)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+: Request All Files Access for managing external storage
if (!android.os.Environment.isExternalStorageManager()) {
try {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = android.net.Uri.fromParts("package", packageName, null)
intent.data = uri
startActivity(intent)
} catch (_: Exception) {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
startActivity(intent)
}
}
} else {
// Android 10 and below
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissions.add(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissions.add(android.Manifest.permission.READ_EXTERNAL_STORAGE)
}
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_MEDIA_IMAGES) != PackageManager.PERMISSION_GRANTED) {
permissions.add(android.Manifest.permission.READ_MEDIA_IMAGES)
}
} else {
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissions.add(android.Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
}
if (permissions.isNotEmpty()) {
@ -69,6 +86,15 @@ class HomeActivity : AppCompatActivity() {
}
private fun setupUI() {
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
// Menu Button Logic
val drawerLayout = findViewById<DrawerLayout>(R.id.drawer_layout)
val btnMenu = findViewById<ImageButton>(R.id.btnMenu)
btnMenu?.setOnClickListener {
drawerLayout.openDrawer(GravityCompat.START)
}
// Language Spinner
val languageSpinner = findViewById<Spinner>(R.id.spinnerLanguage)
val languages = StringProvider.getLanguages()
@ -76,7 +102,6 @@ class HomeActivity : AppCompatActivity() {
languageAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
languageSpinner.adapter = languageAdapter
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
val savedLang = prefs.getString("LANGUAGE", "English")
languageSpinner.setSelection(languages.indexOf(savedLang))
@ -99,7 +124,8 @@ class HomeActivity : AppCompatActivity() {
}
// Set text from StringProvider
findViewById<TextView>(R.id.tvTitle).text = StringProvider.getString("title_home")
findViewById<TextView>(R.id.tvTitle).text = StringProvider.getString("app_name")
findViewById<TextView>(R.id.tvSubtitle).text = StringProvider.getString("subtitle_home")
findViewById<MaterialButton>(R.id.btnViewGallery).text = StringProvider.getString("btn_view_gallery")
findViewById<MaterialButton>(R.id.btnSelectCow).text = StringProvider.getString("btn_select_cow")
findViewById<TextView>(R.id.tvAlgorithmLabel).text = StringProvider.getString("label_algorithm")
@ -120,19 +146,81 @@ class HomeActivity : AppCompatActivity() {
}
findViewById<MaterialButton>(R.id.btnSelectCow).setOnClickListener {
saveSettingsAndStart()
// if (!IS_RELEASE_BUILD) { // This logic seemed wrong in previous thought, we want to start activity regardless?
// Ah, user said "dont show the settings panel". It doesn't mean disable "Add Cow".
// But previously I wrote logic to save settings before start.
if (!IS_RELEASE_BUILD) {
saveSettings()
}
startActivity(Intent(this, CowSelectionActivity::class.java))
}
// Auto Capture Toggle
val switchAutoCapture = findViewById<SwitchMaterial>(R.id.switchAutoCapture)
switchAutoCapture.text = StringProvider.getString("label_auto_capture")
val autoCaptureEnabled = prefs.getBoolean(PREF_AUTO_CAPTURE, false)
switchAutoCapture.isChecked = autoCaptureEnabled
val states = arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
)
val thumbColors = intArrayOf(
Color.parseColor("#6D4C41"), // Dark Brown for On
Color.GRAY // Grey for Off
)
val trackColors = intArrayOf(
Color.parseColor("#8D6E63"), // Lighter Brown for On track
Color.LTGRAY // Light Grey for Off track
)
switchAutoCapture.thumbTintList = ColorStateList(states, thumbColors)
switchAutoCapture.trackTintList = ColorStateList(states, trackColors)
switchAutoCapture.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean(PREF_AUTO_CAPTURE, isChecked).apply()
}
val switchEnableDebug = findViewById<SwitchMaterial>(R.id.switchEnableDebug)
switchEnableDebug.text = StringProvider.getString("label_enable_debug")
val debugEnabled = prefs.getBoolean(PREF_DEBUG_ENABLE, false)
switchEnableDebug.isChecked = debugEnabled
IS_RELEASE_BUILD = !debugEnabled
switchEnableDebug.thumbTintList = ColorStateList(states, thumbColors)
switchEnableDebug.trackTintList = ColorStateList(states, trackColors)
switchEnableDebug.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean(PREF_DEBUG_ENABLE, isChecked).apply()
IS_RELEASE_BUILD = !isChecked
if (IS_RELEASE_BUILD) {
findViewById<CardView>(R.id.viewSettings).visibility = View.GONE
} else {
saveSettings()
findViewById<CardView>(R.id.viewSettings).visibility = View.VISIBLE
}
}
val headerSettings = findViewById<TextView>(R.id.tvHeaderSettings)
if (headerSettings != null) headerSettings.text = StringProvider.getString("header_settings")
// Algorithm Spinner
val spinner = findViewById<Spinner>(R.id.spinnerAlgorithm)
val algorithms = listOf(ALGORITHM_HAMMING, ALGORITHM_EUCLIDEAN, ALGORITHM_JACCARD)
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, algorithms)
val displayAlgorithms = listOf(
StringProvider.getString("algo_hamming"),
StringProvider.getString("algo_euclidean"),
StringProvider.getString("algo_jaccard")
)
val adapter = ArrayAdapter(this, android.R.layout.simple_spinner_item, displayAlgorithms)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter
// Set default selection from preferences or intent
val savedAlg = prefs.getString("ALGORITHM", ALGORITHM_HAMMING)
spinner.setSelection(algorithms.indexOf(savedAlg))
val index = internalAlgorithms.indexOf(savedAlg)
spinner.setSelection(if (index >= 0) index else 0)
// Threshold SeekBar
val seekBar = findViewById<SeekBar>(R.id.seekBarThreshold)
@ -149,6 +237,35 @@ class HomeActivity : AppCompatActivity() {
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
})
// Mask Display Toggle
val switchMaskDisplay = findViewById<SwitchMaterial>(R.id.switchMaskDisplay)
switchMaskDisplay.text = StringProvider.getString("label_mask_display")
val maskDisplayEnabled = prefs.getBoolean(PREF_MASK_DISPLAY, false)
switchMaskDisplay.isChecked = maskDisplayEnabled
switchMaskDisplay.thumbTintList = ColorStateList(states, thumbColors)
switchMaskDisplay.trackTintList = ColorStateList(states, trackColors)
switchMaskDisplay.setOnCheckedChangeListener { _, isChecked ->
prefs.edit().putBoolean(PREF_MASK_DISPLAY, isChecked).apply()
}
// Initial visibility based on IS_RELEASE_BUILD
if (IS_RELEASE_BUILD) {
val settingsView = findViewById<CardView>(R.id.viewSettings)
settingsView.visibility = View.GONE
// Force defaults if needed, but logic above already sets defaults from prefs or code constants.
// If user wants to FORCE defaults every time release build runs:
/*
prefs.edit().putString("ALGORITHM", ALGORITHM_JACCARD).apply()
prefs.edit().putInt("THRESHOLD", 88).apply()
prefs.edit().putBoolean(PREF_MASK_DISPLAY, false).apply()
*/
// Leaving as is based on previous logic.
} else {
findViewById<CardView>(R.id.viewSettings).visibility = View.VISIBLE
}
}
private fun showIllustrationPopup(anchor: View, imageView: ImageView) {
@ -171,22 +288,13 @@ class HomeActivity : AppCompatActivity() {
}
private fun setCowIllustration(imageView: ImageView, index: Int) {
// Map index to drawable resource
// We assume resources are named cow_illustration (for index 0 or default) or similar logic if strictly named
// But user said "cow_illustration_{n}, where n=0,1,2,3,4"
// However, currently we likely only have 'cow_illustration' in drawable from previous step, or user implies they exist.
// We will try to find resources dynamically.
val resName = "cow_illustration_$index"
val resId = resources.getIdentifier(resName, "drawable", packageName)
if (resId != 0) {
imageView.setImageResource(resId)
} else {
// Fallback to default if specific one not found
if (index == 0) {
// Maybe it's just "cow_illustration" without suffix if n=0 is base?
// Or user provided 5 distinct files. We try 'cow_illustration' as fallback.
val defaultId = resources.getIdentifier("cow_illustration", "drawable", packageName)
if (defaultId != 0) imageView.setImageResource(defaultId)
}
@ -196,23 +304,33 @@ class HomeActivity : AppCompatActivity() {
private fun saveSettings() {
val spinner = findViewById<Spinner>(R.id.spinnerAlgorithm)
val seekBar = findViewById<SeekBar>(R.id.seekBarThreshold)
val showSegmentationMask = findViewById<SwitchMaterial>(R.id.switchMaskDisplay)
if (spinner != null && seekBar != null && showSegmentationMask != null) {
val selectedIndex = spinner.selectedItemPosition
val selectedAlgorithm = if (selectedIndex >= 0 && selectedIndex < internalAlgorithms.size) {
internalAlgorithms[selectedIndex]
} else {
ALGORITHM_HAMMING
}
if (spinner != null && seekBar != null) {
val selectedAlgorithm = spinner.selectedItem?.toString() ?: ALGORITHM_HAMMING
val threshold = seekBar.progress
// Save to preferences
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
val showMask = prefs.getBoolean(PREF_MASK_DISPLAY, false)
prefs.edit().apply {
putString("ALGORITHM", selectedAlgorithm)
putInt("THRESHOLD", threshold)
putBoolean(PREF_MASK_DISPLAY, showMask)
apply()
}
}
}
private fun saveSettingsAndStart() {
saveSettings()
if(!IS_RELEASE_BUILD)
saveSettings()
startActivity(Intent(this, CowSelectionActivity::class.java))
}
}

View File

@ -0,0 +1,328 @@
package com.example.animalrating
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.Gravity
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.TextInputLayout
import java.io.File
import java.io.FileWriter
import java.util.Locale
class RatingActivity : AppCompatActivity() {
private lateinit var currentCowName: String
private lateinit var ratingsContainer: LinearLayout
// Feature list (internal keys)
private val features = listOf(
"Stature", "Chest width", "Body depth", "Angularity",
"Rump angle", "Rump width", "Rear legs set", "Rear legs rear view",
"Foot angle", "Fore udder attachment", "Rear udder height",
"Central ligament", "Udder depth", "Front teat position",
"Teat length", "Rear teat position", "Locomotion",
"Body condition score", "Hock development", "Bone structure",
"Rear udder width", "Teat thickness", "Muscularity"
)
private val ratingsMap = mutableMapOf<String, Int>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_rating)
StringProvider.initialize(this)
currentCowName = intent.getStringExtra("COW_NAME") ?: run {
finish()
return
}
val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
toolbar.setNavigationOnClickListener { finish() }
findViewById<TextView>(R.id.tvToolbarTitle)?.text = StringProvider.getString("title_rate_cow")
setupUIStrings()
loadCowDetails()
loadCowImages()
setupRatingSection()
loadExistingRatings()
findViewById<MaterialButton>(R.id.btnSaveRating).setOnClickListener {
saveRatings()
}
findViewById<MaterialButton>(R.id.btnCancelRating).setOnClickListener {
finish()
}
}
private fun setupUIStrings() {
findViewById<TextView>(R.id.tvHeaderPhotos)?.text = StringProvider.getString("header_photos")
findViewById<TextView>(R.id.tvHeaderCowDetails)?.text = StringProvider.getString("header_cow_details")
findViewById<TextView>(R.id.tvHeaderFeatureRatings)?.text = StringProvider.getString("header_feature_ratings")
findViewById<TextInputLayout>(R.id.tilComments)?.hint = StringProvider.getString("hint_comments")
findViewById<MaterialButton>(R.id.btnSaveRating)?.text = StringProvider.getString("btn_save_rating")
findViewById<MaterialButton>(R.id.btnCancelRating)?.text = StringProvider.getString("btn_cancel")
}
private fun loadCowDetails() {
val docsFolder = StorageUtils.getDocumentsFolder()
val csvFile = File(docsFolder, "cow_profiles.csv")
if (!csvFile.exists()) return
try {
val lines = csvFile.readLines()
val record = lines.find { it.startsWith("$currentCowName,") }?.split(",") ?: return
if (record.size >= 8) {
// Translate values for display
val storedSpecies = record[1]
val speciesKey = StringProvider.getKeyForEnglishValue(storedSpecies)
val displaySpecies = if (speciesKey != null) StringProvider.getString(speciesKey) else storedSpecies
val storedBreed = record[2]
val breedKey = StringProvider.getKeyForEnglishValue(storedBreed)
val displayBreed = if (breedKey != null) StringProvider.getString(breedKey) else storedBreed
val storedStatus = record[6]
val statusKey = StringProvider.getKeyForEnglishValue(storedStatus)
val displayStatus = if (statusKey != null) StringProvider.getString(statusKey) else storedStatus
val infoText = StringBuilder()
infoText.append("${StringProvider.getString("label_species")} $displaySpecies\n")
infoText.append("${StringProvider.getString("label_breed")} $displayBreed\n")
infoText.append("${StringProvider.getString("label_age")} ${record[3]} ${StringProvider.getString("unit_years")}\n")
infoText.append("${StringProvider.getString("label_milk_yield")} ${record[4]} ${StringProvider.getString("unit_liters")}\n")
infoText.append("${StringProvider.getString("label_calving_no")} ${record[5]}\n")
infoText.append("${StringProvider.getString("label_status")} $displayStatus\n")
infoText.append("${StringProvider.getString("hint_description")}: ${record[7]}")
findViewById<TextView>(R.id.tvCowDetails).text = infoText.toString()
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun loadCowImages() {
val container = findViewById<LinearLayout>(R.id.ratingImagesContainer) ?: return
container.removeAllViews()
val cowImagesFolder = StorageUtils.getCowImageFolder(currentCowName)
val files = cowImagesFolder.listFiles { _, name -> name.startsWith("${currentCowName}_") && name.endsWith(".jpg") } ?: return
val horizontalScroll = android.widget.HorizontalScrollView(this)
horizontalScroll.layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
horizontalScroll.isFillViewport = false
val imagesLayout = LinearLayout(this)
imagesLayout.orientation = LinearLayout.HORIZONTAL
files.forEach { file ->
val imageView = ImageView(this)
val size = (100 * resources.displayMetrics.density).toInt()
val params = LinearLayout.LayoutParams(size, size)
params.setMargins(0, 0, 16, 0)
imageView.layoutParams = params
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
imageView.setImageBitmap(bitmap)
imageView.setOnClickListener {
val intent = Intent(this, FullScreenImageActivity::class.java)
intent.putExtra("IMAGE_PATH", file.absolutePath)
startActivity(intent)
}
imagesLayout.addView(imageView)
}
horizontalScroll.addView(imagesLayout)
container.addView(horizontalScroll)
}
private fun setupRatingSection() {
ratingsContainer = findViewById(R.id.ratingsContainer)
ratingsContainer.removeAllViews()
features.forEach { feature ->
val featureView = layoutInflater.inflate(R.layout.item_feature_rating, ratingsContainer, false)
// Localize feature name
val key = "feature_" + feature.lowercase(Locale.ROOT).replace(" ", "_")
val displayName = StringProvider.getString(key)
val finalName = if (displayName.isNotEmpty()) displayName else feature
featureView.findViewById<TextView>(R.id.tvFeatureName).text = finalName
val buttonContainer = featureView.findViewById<LinearLayout>(R.id.buttonContainer)
val segmentViews = mutableListOf<TextView>()
// Create 9 segments
for (i in 1..9) {
val tv = TextView(this)
val params = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.MATCH_PARENT,
1f
)
tv.layoutParams = params
tv.text = i.toString()
tv.gravity = Gravity.CENTER
tv.setTextColor(ContextCompat.getColor(this, R.color.black))
tv.textSize = 14f
tv.setBackgroundColor(Color.TRANSPARENT)
tv.setOnClickListener {
val currentRating = ratingsMap[feature] ?: 0
if (currentRating == i) {
// Clicked already selected -> Clear selection
ratingsMap[feature] = 0
updateSegmentSelection(segmentViews, 0)
} else {
// Select new rating
ratingsMap[feature] = i
updateSegmentSelection(segmentViews, i)
}
}
segmentViews.add(tv)
buttonContainer.addView(tv)
}
featureView.tag = segmentViews
ratingsContainer.addView(featureView)
}
}
private fun updateSegmentSelection(segments: List<TextView>, selectedRating: Int) {
segments.forEachIndexed { index, tv ->
val rating = index + 1
if (rating == selectedRating) {
tv.setTextColor(Color.WHITE)
val radius = (8 * resources.displayMetrics.density)
val drawable = GradientDrawable()
drawable.setColor(Color.parseColor("#6D4C41"))
if (rating == 1) {
drawable.cornerRadii = floatArrayOf(radius, radius, 0f, 0f, 0f, 0f, radius, radius)
} else if (rating == 9) {
drawable.cornerRadii = floatArrayOf(0f, 0f, radius, radius, radius, radius, 0f, 0f)
} else {
drawable.cornerRadius = 0f
}
tv.background = drawable
} else {
tv.setTextColor(Color.parseColor("#5D4037"))
tv.background = null
}
}
}
private fun loadExistingRatings() {
val docsFolder = StorageUtils.getDocumentsFolder()
val ratingsFile = File(docsFolder, "cow_ratings.csv")
if (!ratingsFile.exists()) return
try {
val lines = ratingsFile.readLines()
// Format: CowID,Comments,Feature1,Feature2,...
val record = lines.find { it.startsWith("$currentCowName,") }?.split(",") ?: return
if (record.size >= 2) {
// Index 0: ID
// Index 1: Comments
val comments = record[1].replace(";", ",")
findViewById<TextInputLayout>(R.id.tilComments).editText?.setText(comments)
// Ratings start from index 2
features.forEachIndexed { index, feature ->
val ratingStr = record.getOrNull(index + 2)
val rating = ratingStr?.toIntOrNull() ?: 0
if (rating > 0) {
ratingsMap[feature] = rating
// Find view and update by index
val featureView = ratingsContainer.getChildAt(index)
if (featureView != null) {
@Suppress("UNCHECKED_CAST")
val segments = featureView.tag as? List<TextView>
segments?.let { updateSegmentSelection(it, rating) }
}
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun saveRatings() {
val commentsInput = findViewById<TextInputLayout>(R.id.tilComments).editText?.text.toString()
val comments = commentsInput.replace(",", ";")
val docsFolder = StorageUtils.getDocumentsFolder()
val ratingsFile = File(docsFolder, "cow_ratings.csv")
// Dynamically build header
val headerBuilder = StringBuilder("CowID,Comments")
features.forEach { headerBuilder.append(",$it") }
val header = headerBuilder.toString()
// Build row
val rowBuilder = StringBuilder()
rowBuilder.append("$currentCowName,$comments")
features.forEach { feature ->
val rating = ratingsMap[feature] ?: 0 // Defaults to 0 if not selected
rowBuilder.append(",$rating")
}
val newRow = rowBuilder.toString()
try {
val lines = if (ratingsFile.exists()) ratingsFile.readLines().toMutableList() else mutableListOf()
if (lines.isEmpty()) {
lines.add(header)
} else if (lines[0] != header) {
// Header mismatch handling
}
val existingIndex = lines.indexOfFirst { it.startsWith("$currentCowName,") }
if (existingIndex != -1) {
lines[existingIndex] = newRow
} else {
lines.add(newRow)
}
FileWriter(ratingsFile).use { writer ->
lines.forEach { line ->
writer.write(line + "\n")
}
}
Toast.makeText(this, StringProvider.getString("toast_ratings_saved"), Toast.LENGTH_SHORT).show()
finish()
} catch (e: Exception) {
Toast.makeText(this, StringProvider.getString("toast_error_saving_ratings"), Toast.LENGTH_SHORT).show()
}
}
}

View File

@ -0,0 +1,46 @@
package com.example.animalrating
import android.os.Environment
import java.io.File
object StorageUtils {
private const val ROOT_FOLDER_NAME = "com.AnimalRating"
private fun getBaseFolder(): File {
// Changed to Android/media/com.AnimalRating as requested
val folder = File(Environment.getExternalStorageDirectory(), "Android/media/$ROOT_FOLDER_NAME")
if (!folder.exists()) {
folder.mkdirs()
}
return folder
}
fun getDocumentsFolder(): File {
val folder = File(getBaseFolder(), "Documents")
if (!folder.exists()) {
folder.mkdirs()
}
return folder
}
fun getImagesBaseFolder(): File {
val folder = File(getBaseFolder(), "Images")
if (!folder.exists()) {
folder.mkdirs()
}
return folder
}
fun getCowImageFolder(cowId: String): File {
return File(getImagesBaseFolder(), cowId)
}
fun getVideosFolder(): File {
val folder = File(getBaseFolder(), "Videos")
if (!folder.exists()) {
folder.mkdirs()
}
return folder
}
}

View File

@ -43,32 +43,19 @@ class SilhouetteOverlay(context: Context, attrs: AttributeSet?) : View(context,
val bmpW = bmp.width.toFloat()
val bmpH = bmp.height.toFloat()
val viewAspect = viewW / viewH
val bmpAspect = bmpW / bmpH
// Calculate scale to fit (FIT_CENTER)
val scale = kotlin.math.min(viewW / bmpW, viewH / bmpH)
val cropRect: RectF
val scaledW = bmpW * scale
val scaledH = bmpH * scale
if (bmpAspect > viewAspect) {
// Bitmap is wider → crop left & right
val newW = bmpH * viewAspect
val left = (bmpW - newW) / 2f
cropRect = RectF(left, 0f, left + newW, bmpH)
} else {
// Bitmap is taller → crop top & bottom
val newH = bmpW / viewAspect
val top = (bmpH - newH) / 2f
cropRect = RectF(0f, top, bmpW, top + newH)
}
val left = (viewW - scaledW) / 2f
val top = (viewH - scaledH) / 2f
// Scale to exactly fill screen
val destRect = RectF(0f, 0f, viewW, viewH)
val destRect = RectF(left, top, left + scaledW, top + scaledH)
val srcRect = Rect(0, 0, bmp.width, bmp.height)
canvas.drawBitmap(bmp, cropRect.toRect(), destRect, silhouettePaint)
canvas.drawBitmap(bmp, srcRect, destRect, silhouettePaint)
}
}
// Helper
private fun RectF.toRect(): Rect {
return Rect(left.toInt(), top.toInt(), right.toInt(), bottom.toInt())
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="#5D4037" />
<solid android:color="#FFFFFF" />
</shape>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:width="1dp" />
<solid android:color="#5D4037" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@ -46,6 +46,15 @@
android:layout_marginBottom="24dp"/>
<!-- Form Fields -->
<TextView
android:id="@+id/tvLabelSpecies"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Species"
android:textColor="#5D4037"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilSpecies"
android:layout_width="match_parent"
@ -56,8 +65,7 @@
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41"
android:hint="Species">
app:boxStrokeColor="#6D4C41">
<AutoCompleteTextView
android:id="@+id/spinnerSpecies"
@ -66,6 +74,14 @@
android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvLabelBreed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Breed"
android:textColor="#5D4037"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilBreed"
android:layout_width="match_parent"
@ -76,8 +92,7 @@
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41"
android:hint="Breed">
app:boxStrokeColor="#6D4C41">
<AutoCompleteTextView
android:id="@+id/spinnerBreed"
@ -93,48 +108,82 @@
android:baselineAligned="false"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAge"
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41"
android:hint="Age (Years)">
android:orientation="vertical">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAge"
<TextView
android:id="@+id/tvLabelAge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Age (Years)"
android:textColor="#5D4037"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilAge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilMilk"
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etAge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41"
android:hint="Milk Yield (L)">
android:orientation="vertical">
<com.google.android.material.textfield.TextInputEditText
<TextView
android:id="@+id/tvLabelMilk"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Milk Yield (L)"
android:textColor="#5D4037"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilMilk"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"/>
</com.google.android.material.textfield.TextInputLayout>
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="numberDecimal"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/tvLabelCalving"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Calving Number"
android:textColor="#5D4037"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilCalving"
android:layout_width="match_parent"
@ -145,8 +194,7 @@
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41"
android:hint="Calving Number">
app:boxStrokeColor="#6D4C41">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
@ -190,6 +238,14 @@
android:text="None"/>
</RadioGroup>
<TextView
android:id="@+id/tvLabelDescription"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Description"
android:textColor="#5D4037"
android:layout_marginBottom="4dp"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilDescription"
android:layout_width="match_parent"
@ -200,8 +256,7 @@
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41"
android:hint="Description">
app:boxStrokeColor="#6D4C41">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
@ -225,7 +280,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:columnCount="2"
android:rowCount="3"
android:rowCount="4"
android:alignmentMode="alignMargins"
android:columnOrderPreserved="false"
android:layout_marginBottom="24dp">
@ -362,12 +417,77 @@
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Angle View -->
<!-- Left Angle -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnLeftAngle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="#F5F5F5"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_camera"
app:tint="#5D4037"/>
<TextView
android:id="@+id/tvLeftAngle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Left Angle"
android:textSize="12sp"
android:textColor="#5D4037"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Right Angle -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<LinearLayout
android:id="@+id/btnRightAngle"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="#F5F5F5"
android:clickable="true"
android:focusable="true">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_camera"
app:tint="#5D4037"/>
<TextView
android:id="@+id/tvRightAngle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Right Angle"
android:textSize="12sp"
android:textColor="#5D4037"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Angle View (Top) - Fixed Size, No Span -->
<androidx.cardview.widget.CardView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_columnWeight="1"
android:layout_columnSpan="2"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">

View File

@ -25,4 +25,14 @@
android:scaleType="centerInside"
android:contentDescription="Back" />
<Button
android:id="@+id/btnRetake"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="32dp"
android:text="Retake"
android:visibility="gone" />
</RelativeLayout>

View File

@ -1,198 +1,287 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F7F7F7"
android:fillViewport="true">
tools:openDrawer="start">
<LinearLayout
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center_horizontal">
android:layout_height="match_parent"
android:background="#F7F7F7"
android:fillViewport="true">
<!-- Language Selection (Top Right) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:layout_marginBottom="16dp">
<Spinner
android:id="@+id/spinnerLanguage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@drawable/bg_spinner_rounded"/>
</LinearLayout>
<!-- Cow Illustration - Circular Logo -->
<androidx.cardview.widget.CardView
android:layout_width="180dp"
android:layout_height="180dp"
app:cardCornerRadius="90dp"
app:cardElevation="8dp"
app:cardBackgroundColor="#FFFFFF"
android:layout_marginBottom="24dp">
<ImageView
android:id="@+id/ivCowIllustration"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/cow_illustration_0"
android:scaleType="centerCrop"
android:contentDescription="Cow Illustration"/>
</androidx.cardview.widget.CardView>
<!-- Title -->
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cow Data Collector"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="#3E2723"
android:textAlignment="center"
android:layout_marginBottom="8dp"/>
<!-- Subtitle -->
<TextView
android:id="@+id/tvSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Record and manage cow information easily."
android:textSize="16sp"
android:textColor="#6D4C41"
android:textAlignment="center"
android:layout_marginBottom="40dp"/>
<!-- Main Buttons Container -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="32dp">
android:padding="24dp"
android:gravity="center_horizontal">
<!-- Add New Cow Profile Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSelectCow"
<!-- Menu Button (Top Right) to Open Drawer -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="72dp"
android:text="Add New Cow Profile"
android:textSize="18sp"
android:layout_height="wrap_content"
android:gravity="end"
android:layout_marginBottom="16dp">
<ImageButton
android:id="@+id/btnMenu"
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@android:drawable/ic_menu_sort_by_size"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Menu"
app:tint="#5D4037"/>
</LinearLayout>
<!-- Cow Illustration - Circular Logo -->
<androidx.cardview.widget.CardView
android:layout_width="180dp"
android:layout_height="180dp"
app:cardCornerRadius="90dp"
app:cardElevation="8dp"
app:cardBackgroundColor="#FFFFFF"
android:layout_marginBottom="24dp">
<ImageView
android:id="@+id/ivCowIllustration"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/cow_illustration_0"
android:scaleType="centerCrop"
android:contentDescription="Cow Illustration"/>
</androidx.cardview.widget.CardView>
<!-- Title -->
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cow Data Collector"
android:textSize="28sp"
android:textStyle="bold"
app:cornerRadius="16dp"
app:backgroundTint="#6D4C41"
app:icon="@android:drawable/ic_input_add"
app:iconGravity="textStart"
app:iconPadding="12dp"
android:layout_marginBottom="16dp"
android:elevation="4dp"/>
android:textColor="#3E2723"
android:textAlignment="center"
android:layout_marginBottom="8dp"/>
<!-- View Saved Profiles Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnViewGallery"
android:layout_width="match_parent"
android:layout_height="72dp"
android:text="View Saved Profiles"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#5D4037"
app:cornerRadius="16dp"
app:backgroundTint="#EFEBE9"
app:strokeColor="#5D4037"
app:strokeWidth="1dp"
app:icon="@android:drawable/ic_menu_gallery"
app:iconTint="#5D4037"
app:iconGravity="textStart"
app:iconPadding="12dp"
android:elevation="4dp"/>
</LinearLayout>
<!-- Settings Card -->
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#FFFFFF"
android:layout_marginBottom="24dp">
<!-- Subtitle -->
<TextView
android:id="@+id/tvSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Record and manage cow information easily."
android:textSize="16sp"
android:textColor="#6D4C41"
android:textAlignment="center"
android:layout_marginBottom="40dp"/>
<!-- Main Buttons Container -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
android:layout_marginBottom="32dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Settings"
<!-- Add New Cow Profile Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSelectCow"
android:layout_width="match_parent"
android:layout_height="72dp"
android:text="Add New Cow Profile"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#3E2723"
android:layout_marginBottom="16dp"/>
app:cornerRadius="16dp"
app:backgroundTint="#6D4C41"
app:icon="@android:drawable/ic_input_add"
app:iconGravity="textStart"
app:iconPadding="12dp"
android:layout_marginBottom="16dp"
android:elevation="4dp"/>
<!-- Algorithm Selection -->
<TextView
android:id="@+id/tvAlgorithmLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Algorithm"
android:textSize="14sp"
android:textColor="#5D4037"
android:layout_marginBottom="8dp"/>
<FrameLayout
<!-- View Saved Profiles Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnViewGallery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_spinner_rounded"
android:padding="4dp"
android:layout_marginBottom="20dp">
<Spinner
android:id="@+id/spinnerAlgorithm"
android:layout_width="match_parent"
android:layout_height="48dp"/>
</FrameLayout>
android:layout_height="72dp"
android:text="View Saved Profiles"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#5D4037"
app:cornerRadius="16dp"
app:backgroundTint="#EFEBE9"
app:strokeColor="#5D4037"
app:strokeWidth="1dp"
app:icon="@android:drawable/ic_menu_gallery"
app:iconTint="#5D4037"
app:iconGravity="textStart"
app:iconPadding="12dp"
android:elevation="4dp"/>
</LinearLayout>
<!-- Settings Card -->
<androidx.cardview.widget.CardView
android:id="@+id/viewSettings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:cardBackgroundColor="#FFFFFF"
android:layout_marginBottom="24dp">
<!-- Threshold Slider -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/tvThresholdLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Match Threshold"
android:textSize="14sp"
android:textColor="#5D4037"/>
<TextView
android:id="@+id/tvThresholdValue"
android:id="@+id/tvHeaderSettings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="75%"
android:text="Settings"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#3E2723"/>
</LinearLayout>
android:textColor="#3E2723"
android:layout_marginBottom="16dp"/>
<SeekBar
android:id="@+id/seekBarThreshold"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:progressTint="#6D4C41"
android:thumbTint="#6D4C41"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- Algorithm Selection -->
<TextView
android:id="@+id/tvAlgorithmLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Algorithm"
android:textSize="14sp"
android:textColor="#5D4037"
android:layout_marginBottom="8dp"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_spinner_rounded"
android:padding="4dp"
android:layout_marginBottom="20dp">
<Spinner
android:id="@+id/spinnerAlgorithm"
android:layout_width="match_parent"
android:layout_height="48dp"/>
</FrameLayout>
<!-- Threshold Slider -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/tvThresholdLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Match Threshold"
android:textSize="14sp"
android:textColor="#5D4037"/>
<TextView
android:id="@+id/tvThresholdValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="75%"
android:textStyle="bold"
android:textColor="#3E2723"/>
</LinearLayout>
<SeekBar
android:id="@+id/seekBarThreshold"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:max="100"
android:progressTint="#6D4C41"
android:thumbTint="#6D4C41"
android:layout_marginBottom="16dp"/>
<!-- Mask Display Toggle -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchMaskDisplay"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Show Segmentation Mask"
android:textColor="#5D4037"
android:textSize="14sp"
app:thumbTint="#6D4C41"
app:trackTint="#EFEBE9"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
<!-- Navigation Drawer Content (Direct LinearLayout) -->
<LinearLayout
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#FFFFFF"
android:orientation="vertical"
android:padding="16dp"
android:clickable="true"
android:focusable="true"
android:elevation="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Menu"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#3E2723"
android:layout_marginBottom="24dp"/>
<!-- Language Selection -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Language"
android:textColor="#5D4037"
android:layout_marginBottom="8dp"/>
<Spinner
android:id="@+id/spinnerLanguage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@drawable/bg_spinner_rounded"
android:layout_marginBottom="24dp"/>
<!-- Auto Capture Toggle -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchAutoCapture"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Auto Capture"
android:textColor="#5D4037"
android:textSize="14sp"
app:thumbTint="#6D4C41"
app:trackTint="#EFEBE9"
android:layout_marginBottom="16dp"/>
<!-- Enable Debug Toggle -->
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchEnableDebug"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Enable Debug"
android:textColor="#5D4037"
android:textSize="14sp"
app:thumbTint="#6D4C41"
app:trackTint="#EFEBE9"
android:layout_marginBottom="16dp"/>
</LinearLayout>
</ScrollView>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -13,7 +13,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- ImageView to display the saved mask (Green) -->
<!-- ImageView to display the saved mask (Green)-->
<ImageView
android:id="@+id/savedMaskOverlay"
android:layout_width="match_parent"
@ -21,7 +21,7 @@
android:scaleType="centerCrop"
android:elevation="5dp"/>
<!-- ImageView to display the segmentation mask (Live) -->
<!-- ImageView to display the segmentation mask (Live)-->
<ImageView
android:id="@+id/segmentationOverlay"
android:layout_width="match_parent"
@ -51,4 +51,51 @@
app:iconPadding="0dp"
android:gravity="center"/>
<!-- Toggle Auto/Manual Capture Button -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnToggleCaptureMode"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="top|end"
android:layout_margin="16dp"
style="@style/Widget.MaterialComponents.Button.Icon"
app:icon="@android:drawable/ic_menu_camera"
app:iconSize="24dp"
app:iconTint="#FFFFFF"
app:backgroundTint="#80000000"
app:cornerRadius="24dp"
android:padding="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:iconGravity="textStart"
app:iconPadding="0dp"
android:gravity="center"/>
<!-- Shutter Button (Initially hidden if Auto) -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnShutter"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="32dp"
style="@style/Widget.MaterialComponents.Button.Icon"
app:icon="@android:drawable/ic_menu_camera"
app:iconSize="40dp"
app:iconTint="#FFFFFF"
app:backgroundTint="#D32F2F"
app:cornerRadius="36dp"
app:strokeColor="#FFFFFF"
app:strokeWidth="4dp"
android:padding="0dp"
android:insetLeft="0dp"
android:insetTop="0dp"
android:insetRight="0dp"
android:insetBottom="0dp"
app:iconGravity="textStart"
app:iconPadding="0dp"
android:gravity="center"
android:visibility="gone"/>
</FrameLayout>

View File

@ -0,0 +1,150 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
android:background="#FFFFFF">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#FFFFFF"
app:navigationIcon="@drawable/ic_round_back_button">
<TextView
android:id="@+id/tvToolbarTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Rate Cow"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="#3E2723"/>
</androidx.appcompat.widget.Toolbar>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:id="@+id/tvHeaderPhotos"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Photos"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#3E2723"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:id="@+id/ratingImagesContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp"
android:minHeight="100dp"/>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="12dp"
app:cardElevation="2dp"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp"
android:background="#FAFAFA">
<TextView
android:id="@+id/tvHeaderCowDetails"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cow Details"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#5D4037"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tvCowDetails"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="#3E2723"
android:lineSpacingExtra="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilComments"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41"
android:hint="Comments">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:minLines="3"
android:gravity="top"/>
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/tvHeaderFeatureRatings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Feature Ratings"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#3E2723"
android:layout_marginBottom="16dp"/>
<LinearLayout
android:id="@+id/ratingsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="24dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnSaveRating"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="Save Rating"
android:textSize="16sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:backgroundTint="#6D4C41"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnCancelRating"
android:layout_width="match_parent"
android:layout_height="56dp"
android:text="Cancel"
android:textSize="16sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:backgroundTint="#6D4C41"/>
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/tvFeatureName"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="#5D4037"
android:layout_marginBottom="8dp"/>
<!-- Rounded Rectangle Container for Segments -->
<LinearLayout
android:id="@+id/buttonContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:background="@drawable/bg_rating_segment"
android:weightSum="9"
android:showDividers="middle"
android:divider="@drawable/divider_rating_segment"/>
</LinearLayout>

View File

@ -6,8 +6,8 @@
android:layout_margin="4dp">
<androidx.cardview.widget.CardView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
@ -20,6 +20,18 @@
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/tvOrientationLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="#80000000"
android:textColor="@android:color/white"
android:textSize="10sp"
android:padding="2dp"
android:gravity="center"
android:elevation="4dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDelete"
style="@style/Widget.MaterialComponents.Button.Icon"
@ -39,6 +51,7 @@
android:insetBottom="0dp"
app:iconGravity="textStart"
app:iconPadding="0dp"
android:gravity="center"/>
android:gravity="center"
android:elevation="6dp"/>
</FrameLayout>

View File

@ -5,15 +5,34 @@
"title_gallery": "Saved Cow Profiles",
"title_add_cow_details": "Add Cow Details",
"title_cow_selection": "Cow Selection",
"title_rate_cow": "Rate Cow",
"header_photos": "Photos",
"header_cow_details": "Cow Details",
"header_feature_ratings": "Feature Ratings",
"header_settings": "Settings",
"subtitle_home": "Record and manage cow information easily.",
"btn_view_gallery": "View Saved Cow Profiles",
"btn_select_cow": "Select a Cow",
"btn_select_cow": "Add New Cow Profile",
"label_algorithm": "Algorithm",
"label_match_threshold": "Match Threshold",
"label_auto_capture": "Auto Capture",
"label_enable_debug": "Enable Debug",
"label_mask_display": "Show Segmentation Mask",
"btn_edit": "Edit",
"btn_rate": "Rate",
"btn_delete": "Delete",
"btn_save_rating": "Save Rating",
"btn_retake": "Retake Photo",
"btn_exit": "Exit",
"hint_search_profiles": "Search profiles...",
"hint_comments": "Comments",
"toast_image_deleted": "Image deleted",
"toast_error_deleting_image": "Error deleting image",
"toast_ratings_saved": "Ratings Saved!",
"toast_error_saving_ratings": "Error saving ratings",
"toast_profile_deleted": "Cow profile deleted",
"text_cow_id": "Cow ID:",
"text_no_details": "No details available",
"label_species": "Species:",
"label_breed": "Breed:",
"label_age": "Age:",
@ -38,6 +57,8 @@
"text_left_side": "Left Side",
"text_right_side": "Right Side",
"text_angle_view": "Angle View",
"text_left_angle": "Left Angle",
"text_right_angle": "Right Angle",
"btn_save_profile": "Save Profile",
"btn_cancel": "Cancel",
"toast_profile_saved": "Profile Saved!",
@ -55,7 +76,33 @@
"breed_gir": "Gir",
"breed_red_sindhi": "Red Sindhi",
"breed_murrah": "Murrah",
"breed_surti": "Surti"
"breed_surti": "Surti",
"feature_stature": "Stature",
"feature_chest_width": "Chest width",
"feature_body_depth": "Body depth",
"feature_angularity": "Angularity",
"feature_rump_angle": "Rump angle",
"feature_rump_width": "Rump width",
"feature_rear_legs_set": "Rear legs set",
"feature_rear_legs_rear_view": "Rear legs rear view",
"feature_foot_angle": "Foot angle",
"feature_fore_udder_attachment": "Fore udder attachment",
"feature_rear_udder_height": "Rear udder height",
"feature_central_ligament": "Central ligament",
"feature_udder_depth": "Udder depth",
"feature_front_teat_position": "Front teat position",
"feature_teat_length": "Teat length",
"feature_rear_teat_position": "Rear teat position",
"feature_locomotion": "Locomotion",
"feature_body_condition_score": "Body condition score",
"feature_hock_development": "Hock development",
"feature_bone_structure": "Bone structure",
"feature_rear_udder_width": "Rear udder width",
"feature_teat_thickness": "Teat thickness",
"feature_muscularity": "Muscularity",
"algo_hamming": "Hamming Distance",
"algo_euclidean": "Euclidean Distance",
"algo_jaccard": "Jaccard Similarity"
},
"Hindi": {
"app_name": "पशु रेटिंग",
@ -63,15 +110,34 @@
"title_gallery": "सहेजे गए गाय प्रोफाइल",
"title_add_cow_details": "गाय का विवरण जोड़ें",
"title_cow_selection": "गाय का चयन",
"title_rate_cow": "गाय का मूल्यांकन",
"header_photos": "तस्वीरें",
"header_cow_details": "गाय का विवरण",
"header_feature_ratings": "विशेषता रेटिंग",
"header_settings": "सेटिंग्स",
"subtitle_home": "गाय की जानकारी आसानी से रिकॉर्ड और प्रबंधित करें।",
"btn_view_gallery": "सहेजे गए प्रोफाइल देखें",
"btn_select_cow": "एक गाय का चयन करें",
"btn_select_cow": "नई गाय प्रोफ़ाइल जोड़ें",
"label_algorithm": "एल्गोरिदम",
"label_match_threshold": "मैच थ्रेसहोल्ड",
"label_auto_capture": "ऑटो कैप्चर",
"label_enable_debug": "डिबग सक्षम करें",
"label_mask_display": "सेगमेंटेशन मास्क दिखाएं",
"btn_edit": "संपादित करें",
"btn_rate": "रेट करें",
"btn_delete": "हटाएँ",
"btn_save_rating": "रेटिंग सहेजें",
"btn_retake": "फोटो फिर से लें",
"btn_exit": "बाहर जाएं",
"hint_search_profiles": "प्रोफ़ाइल खोजें...",
"hint_comments": "टिप्पणियाँ",
"toast_image_deleted": "छवि हटा दी गई",
"toast_error_deleting_image": "छवि हटाने में त्रुटि",
"toast_ratings_saved": "रेटिंग सहेजी गई!",
"toast_error_saving_ratings": "रेटिंग सहेजने में त्रुटि",
"toast_profile_deleted": "गाय प्रोफ़ाइल हटाई गई",
"text_cow_id": "गाय आईडी:",
"text_no_details": "कोई विवरण उपलब्ध नहीं",
"label_species": "प्रजाति:",
"label_breed": "नस्ल:",
"label_age": "आयु:",
@ -96,6 +162,8 @@
"text_left_side": "बाईं ओर",
"text_right_side": "दाईं ओर",
"text_angle_view": "कोणीय दृश्य",
"text_left_angle": "बायां कोण",
"text_right_angle": "दायां कोण",
"btn_save_profile": "प्रोफ़ाइल सहेजें",
"btn_cancel": "रद्द करें",
"toast_profile_saved": "प्रोफ़ाइल सहेजा गया!",
@ -113,6 +181,32 @@
"breed_gir": "गिर",
"breed_red_sindhi": "लाल सिंधी",
"breed_murrah": "मुर्रा",
"breed_surti": "सुरती"
"breed_surti": "सुरती",
"feature_stature": "कद",
"feature_chest_width": "छाती की चौड़ाई",
"feature_body_depth": "शरीर की गहराई",
"feature_angularity": "कोणीयता",
"feature_rump_angle": "रंप कोण",
"feature_rump_width": "रंप चौड़ाई",
"feature_rear_legs_set": "पिछले पैर सेट",
"feature_rear_legs_rear_view": "पिछले पैर पीछे का दृश्य",
"feature_foot_angle": "पैर का कोण",
"feature_fore_udder_attachment": "आगे का अडर जुड़ाव",
"feature_rear_udder_height": "पीछे के अडर की ऊंचाई",
"feature_central_ligament": "केंद्रीय लिगामेंट",
"feature_udder_depth": "अडर की गहराई",
"feature_front_teat_position": "सामने के थन की स्थिति",
"feature_teat_length": "थन की लंबाई",
"feature_rear_teat_position": "पीछे के थन की स्थिति",
"feature_locomotion": "चाल",
"feature_body_condition_score": "शारीरिक स्थिति स्कोर",
"feature_hock_development": "हॉक विकास",
"feature_bone_structure": "हड्डी की संरचना",
"feature_rear_udder_width": "पीछे के अडर की चौड़ाई",
"feature_teat_thickness": "थन की मोटाई",
"feature_muscularity": "मांसलता",
"algo_hamming": "हैमिंग दूरी",
"algo_euclidean": "यूक्लिडियन दूरी",
"algo_jaccard": "जैकार्ड समानता"
}
}