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> </DropdownSelection>
<DialogSelection /> <DialogSelection />
</SelectionState> </SelectionState>
<SelectionState runConfigName="release">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates> </selectionStates>
</component> </component>
</project> </project>

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package com.example.animalrating
import android.Manifest import android.Manifest
import android.content.ContentValues import android.content.ContentValues
import android.content.Intent
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
@ -12,10 +13,12 @@ import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log import android.util.Log
import android.util.Size import android.util.Size
import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException import androidx.camera.core.ImageCaptureException
@ -49,11 +52,15 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
private var isPhotoTaken = false private var isPhotoTaken = false
private var matchThreshold = 75 private var matchThreshold = 75
private var algorithm = HomeActivity.ALGORITHM_HAMMING private var algorithm = HomeActivity.ALGORITHM_HAMMING
private var isAutoCapture = true
private var isMaskDisplayEnabled = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
StringProvider.initialize(this)
cowName = intent.getStringExtra("COW_NAME") cowName = intent.getStringExtra("COW_NAME")
orientation = intent.getStringExtra("ORIENTATION") orientation = intent.getStringExtra("ORIENTATION")
@ -61,6 +68,8 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE) val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
matchThreshold = prefs.getInt("THRESHOLD", 75) matchThreshold = prefs.getInt("THRESHOLD", 75)
algorithm = prefs.getString("ALGORITHM", HomeActivity.ALGORITHM_HAMMING) ?: HomeActivity.ALGORITHM_HAMMING 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 // Set orientation based on selected view
if (orientation == "front" || orientation == "back") { if (orientation == "front" || orientation == "back") {
@ -77,16 +86,56 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
findViewById<MaterialButton>(R.id.btnExit).setOnClickListener { findViewById<MaterialButton>(R.id.btnExit).setOnClickListener {
finish() 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() frameProcessor = FrameProcessor()
val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0) val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0)
overlay.setSilhouette(silhouetteId) 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) 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() { private fun takePhoto() {
if (isPhotoTaken) return if (isPhotoTaken) return
@ -96,15 +145,22 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
val name = cowName ?: "unknown" val name = cowName ?: "unknown"
val side = orientation ?: "unknown" val side = orientation ?: "unknown"
val silhouetteId = intent.getIntExtra("SILHOUETTE_ID", 0)
// Find current count for this cow and orientation // 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") fname.startsWith("${name}_${side}_") && fname.endsWith(".jpg")
} }
val count = (existingFiles?.size ?: 0) + 1 val count = (existingFiles?.size ?: 0) + 1
if (!cowFolder.exists()) {
cowFolder.mkdirs()
}
val filename = "${name}_${side}_${count}.jpg" val filename = "${name}_${side}_${count}.jpg"
val file = File(filesDir, filename) val file = File(cowFolder, filename)
val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build() val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build()
@ -113,8 +169,8 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
ContextCompat.getMainExecutor(this), ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback { object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) { override fun onError(exc: ImageCaptureException) {
Log.e("MainActivity", "Photo capture failed: ${exc.message}", exc) Log.e("CameraProcessor", "Photo capture failed: ${exc.message}", exc)
Toast.makeText(baseContext, "Photo capture failed", Toast.LENGTH_SHORT).show() Toast.makeText(baseContext, StringProvider.getString("toast_capture_failed"), Toast.LENGTH_SHORT).show()
isPhotoTaken = false isPhotoTaken = false
} }
@ -133,17 +189,31 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
} }
} }
val retakePath = intent.getStringExtra("RETAKE_IMAGE_PATH")
if (!retakePath.isNullOrEmpty()) {
val oldFile = File(retakePath)
if (oldFile.exists()) {
oldFile.delete()
}
}
// Save to public gallery val msg = "${StringProvider.getString("toast_saved_as")} $filename"
saveToGallery(file)
val msg = "Saved as $filename"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() 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() finish()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("MainActivity", "Error saving image", e) Log.e("CameraProcessor", "Error saving image", e)
Toast.makeText(baseContext, "Error saving image", Toast.LENGTH_SHORT).show() Toast.makeText(baseContext, StringProvider.getString("toast_error_saving_image"), Toast.LENGTH_SHORT).show()
isPhotoTaken = false isPhotoTaken = false
} }
} }
@ -152,38 +222,7 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
} }
private fun saveToGallery(file: File) { private fun saveToGallery(file: File) {
val values = ContentValues().apply { // Removed
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)
}
} }
private fun loadSavedMask() { private fun loadSavedMask() {
@ -195,17 +234,50 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
try { try {
val savedBitmap = BitmapFactory.decodeFile(file.absolutePath) val savedBitmap = BitmapFactory.decodeFile(file.absolutePath)
// Apply green color filter for visualization (on original size) if (savedBitmap != null) {
val greenMask = applyGreenColor(savedBitmap) // Apply green color filter for visualization
savedMaskOverlay.setImageBitmap(greenMask) if (isMaskDisplayEnabled) {
savedMaskOverlay.alpha = 0.5f val greenMask = applyGreenColor(savedBitmap)
// Scale to 640x480 (or flipped for portrait) for comparison // Calculate scale to match FIT_CENTER logic of SilhouetteOverlay
val isPortrait = (side == "front" || side == "back") val viewW = savedMaskOverlay.width.toFloat()
val width = if (isPortrait) 480 else 640 val viewH = savedMaskOverlay.height.toFloat()
val height = if (isPortrait) 640 else 480
if (viewW > 0 && viewH > 0) {
savedMaskBitmap = Bitmap.createScaledBitmap(savedBitmap, width, height, true) 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) { } catch (e: Exception) {
Log.e("CameraProcessor", "Error loading saved mask", e) Log.e("CameraProcessor", "Error loading saved mask", e)
@ -269,6 +341,7 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
}, ContextCompat.getMainExecutor(this)) }, ContextCompat.getMainExecutor(this))
} }
@ExperimentalGetImage
override fun onFrame(imageProxy: ImageProxy) { override fun onFrame(imageProxy: ImageProxy) {
if (isPhotoTaken) { if (isPhotoTaken) {
imageProxy.close() imageProxy.close()
@ -282,10 +355,14 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
runOnUiThread { runOnUiThread {
if (result.mask != null) { if (result.mask != null) {
currentMask = result.mask 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() takePhoto()
} }
} }
@ -293,4 +370,4 @@ class CameraProcessor : AppCompatActivity(), CowAnalyzer.CowListener {
Log.e("CameraProcessor", "Frame processing error", e) Log.e("CameraProcessor", "Frame processing error", e)
} }
} }
} }

View File

@ -10,7 +10,6 @@ import android.view.View
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.Button import android.widget.Button
import android.widget.GridLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RadioButton import android.widget.RadioButton
@ -34,51 +33,21 @@ class CowSelectionActivity : AppCompatActivity() {
private var currentCowName: String? = null private var currentCowName: String? = null
private lateinit var imagesContainer: LinearLayout private lateinit var imagesContainer: LinearLayout
private val storagePermissionCode = 101 private val storagePermissionCode = 101
private val orientationViews = mutableMapOf<String, View>()
private val initialImagePaths = mutableSetOf<String>()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_cow_selection) setContentView(R.layout.activity_cow_selection)
// Initialize StringProvider
StringProvider.initialize(this) 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) val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)
// NOTE: The navigation icon is set in XML (@drawable/ic_back_arrow).
// We handle the click here.
toolbar.setNavigationOnClickListener { 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() finish()
} }
@ -87,20 +56,24 @@ class CowSelectionActivity : AppCompatActivity() {
imagesContainer = findViewById(R.id.currentCowImagesContainer) imagesContainer = findViewById(R.id.currentCowImagesContainer)
// Restore cow name if available, or generate new if requested
currentCowName = savedInstanceState?.getString("COW_NAME") ?: intent.getStringExtra("COW_NAME") currentCowName = savedInstanceState?.getString("COW_NAME") ?: intent.getStringExtra("COW_NAME")
if (currentCowName == null) { if (currentCowName == null) {
generateNewCowName() generateNewCowName()
} }
// Try to load existing data if we are editing an existing cow loadInitialImages()
if (intent.hasExtra("COW_NAME")) { if (intent.hasExtra("COW_NAME")) {
loadCowDetails(currentCowName!!) loadCowDetails(currentCowName!!)
} }
updateCowNameDisplay() orientationViews["left"] = findViewById(R.id.btnLeft)
refreshCowImages() 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 { findViewById<Button>(R.id.btnNewCow).setOnClickListener {
if (checkStoragePermissions()) { if (checkStoragePermissions()) {
@ -111,30 +84,70 @@ class CowSelectionActivity : AppCompatActivity() {
} }
findViewById<Button>(R.id.btnCancel).setOnClickListener { findViewById<Button>(R.id.btnCancel).setOnClickListener {
deleteSessionImages()
finish() finish()
} }
}
val buttons = mapOf(
R.id.btnLeft to Pair(R.drawable.left, "left"), private fun loadInitialImages() {
R.id.btnRight to Pair(R.drawable.right, "right"), val name = currentCowName ?: return
R.id.btnTop to Pair(R.drawable.angle, "angle"), val cowFolder = StorageUtils.getCowImageFolder(name)
R.id.btnFront to Pair(R.drawable.front, "front"), if (cowFolder.exists()) {
R.id.btnBack to Pair(R.drawable.back, "back") cowFolder.listFiles()?.forEach { file ->
) if (file.isFile) {
initialImagePaths.add(file.absolutePath)
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 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 { private fun checkStoragePermissions(): Boolean {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
android.os.Environment.isExternalStorageManager() android.os.Environment.isExternalStorageManager()
@ -166,15 +179,9 @@ class CowSelectionActivity : AppCompatActivity() {
} }
private fun saveProfile() { private fun saveProfile() {
// Get displayed values
val speciesDisplay = findViewById<AutoCompleteTextView>(R.id.spinnerSpecies).text.toString() val speciesDisplay = findViewById<AutoCompleteTextView>(R.id.spinnerSpecies).text.toString()
val breedDisplay = findViewById<AutoCompleteTextView>(R.id.spinnerBreed).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 speciesKey = StringProvider.getKeyForValue(speciesDisplay)
val species = if (speciesKey != null) StringProvider.getStringEnglish(speciesKey) else 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 csvHeader = "CowID,Species,Breed,Age,MilkYield,CalvingNumber,ReproductiveStatus,Description\n"
val csvRow = "$currentCowName,$species,$breed,$ageInput,$milkInput,$calvingInput,$reproductiveStatus,$descriptionInput\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 { try {
val fileExists = csvFile.exists() val fileExists = csvFile.exists()
@ -208,7 +216,6 @@ class CowSelectionActivity : AppCompatActivity() {
lines.add(csvHeader.trim()) lines.add(csvHeader.trim())
} }
// Check if we are updating
val existingIndex = lines.indexOfFirst { it.startsWith("$currentCowName,") } val existingIndex = lines.indexOfFirst { it.startsWith("$currentCowName,") }
if (existingIndex != -1) { 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() Toast.makeText(this, StringProvider.getString("toast_profile_saved"), Toast.LENGTH_SHORT).show()
finish() finish()
} catch (e: Exception) { } 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) { 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 if (!csvFile.exists()) return
try { try {
val lines = csvFile.readLines() val lines = csvFile.readLines()
// Removed unused header variable
val record = lines.find { it.startsWith("$cowId,") }?.split(",") ?: return 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) { 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 storedSpecies = record[1]
val speciesKey = StringProvider.getKeyForEnglishValue(storedSpecies) val speciesKey = StringProvider.getKeyForEnglishValue(storedSpecies)
val displaySpecies = if (speciesKey != null) StringProvider.getString(speciesKey) else 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]) findViewById<TextInputLayout>(R.id.tilCalving).editText?.setText(record[5])
val storedStatus = record[6] val storedStatus = record[6]
// Find which key matches this English value
val statusKey = StringProvider.getKeyForEnglishValue(storedStatus) 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) { when(statusKey) {
"radio_pregnant" -> findViewById<RadioButton>(R.id.rbPregnant).isChecked = true "radio_pregnant" -> findViewById<RadioButton>(R.id.rbPregnant).isChecked = true
"radio_calved" -> findViewById<RadioButton>(R.id.rbCalved).isChecked = true "radio_calved" -> findViewById<RadioButton>(R.id.rbCalved).isChecked = true
"radio_none" -> findViewById<RadioButton>(R.id.rbNone).isChecked = true "radio_none" -> findViewById<RadioButton>(R.id.rbNone).isChecked = true
// Fallback if stored directly as text or unknown key
else -> { else -> {
// If stored directly as English "Pregnant" etc.
when (storedStatus) { when (storedStatus) {
"Pregnant" -> findViewById<RadioButton>(R.id.rbPregnant).isChecked = true "Pregnant" -> findViewById<RadioButton>(R.id.rbPregnant).isChecked = true
"Calved" -> findViewById<RadioButton>(R.id.rbCalved).isChecked = true "Calved" -> findViewById<RadioButton>(R.id.rbCalved).isChecked = true
@ -348,7 +317,9 @@ class CowSelectionActivity : AppCompatActivity() {
"right" to R.drawable.right, "right" to R.drawable.right,
"angle" to R.drawable.angle, "angle" to R.drawable.angle,
"front" to R.drawable.front, "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) -> orientationResources.forEach { (orientation, resId) ->
@ -379,10 +350,8 @@ class CowSelectionActivity : AppCompatActivity() {
for (i in pixels.indices) { for (i in pixels.indices) {
val alpha = (pixels[i] shr 24) and 0xFF val alpha = (pixels[i] shr 24) and 0xFF
if (alpha > 0) { if (alpha > 0) {
// Original was visible -> make transparent
pixels[i] = Color.TRANSPARENT pixels[i] = Color.TRANSPARENT
} else { } else {
// Original was transparent -> make black (opaque)
pixels[i] = Color.BLACK pixels[i] = Color.BLACK
} }
} }
@ -394,91 +363,157 @@ class CowSelectionActivity : AppCompatActivity() {
currentCowName = "cow_${sdf.format(Date())}" 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() { private fun refreshCowImages() {
imagesContainer.removeAllViews()
val name = currentCowName ?: return val name = currentCowName ?: return
val files = filesDir.listFiles { _, fname -> fname.startsWith("${name}_") && fname.endsWith(".jpg") } val orientations = mapOf(
if (files.isNullOrEmpty()) return "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 cowImagesFolder = StorageUtils.getCowImageFolder(name)
val filesByOrientation = files.groupBy { file ->
val parts = file.name.split("_") orientations.forEach { (orientation, pair) ->
if (parts.size >= 3) parts[2] else "unknown" val (drawableId, viewId) = pair
}
val files = if (cowImagesFolder.exists()) {
cowImagesFolder.listFiles { _, fname -> fname.startsWith("${name}_${orientation}_") && fname.endsWith(".jpg") }
} else {
null
}
filesByOrientation.forEach { (orientation, orientationFiles) -> val latestFile = files?.maxByOrNull { it.lastModified() }
addOrientationSection(orientation, orientationFiles)
}
}
private fun addOrientationSection(orientationStr: String, files: List<File>) { val container = findViewById<LinearLayout>(viewId)
// Orientation Header container.removeAllViews()
val orientationHeader = TextView(this).apply {
val key = when(orientationStr.lowercase(Locale.getDefault())) { val key = when(orientation) {
"front" -> "text_front_view" "front" -> "text_front_view"
"back" -> "text_rear_view" "back" -> "text_rear_view"
"left" -> "text_left_side" "left" -> "text_left_side"
"right" -> "text_right_side" "right" -> "text_right_side"
"angle" -> "text_angle_view" "angle" -> "text_angle_view"
"leftangle" -> "text_left_angle"
"rightangle" -> "text_right_angle"
else -> "" else -> ""
} }
val label = if (key.isNotEmpty()) StringProvider.getString(key) else orientationStr.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } val label = if (key.isNotEmpty()) StringProvider.getString(key) else orientation
text = label
textSize = 16f
setTypeface(null, android.graphics.Typeface.BOLD)
setPadding(16, 8, 0, 8)
}
imagesContainer.addView(orientationHeader)
// Grid for thumbnails if (latestFile != null && latestFile.exists()) {
val gridLayout = GridLayout(this).apply { val frameLayout = android.widget.FrameLayout(this)
columnCount = 3 frameLayout.layoutParams = LinearLayout.LayoutParams(
layoutParams = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT
LinearLayout.LayoutParams.WRAP_CONTENT )
).apply {
setMargins(16, 0, 16, 16)
}
}
// Images for this orientation val imageView = ImageView(this)
files.forEach { file -> imageView.layoutParams = android.widget.FrameLayout.LayoutParams(
val thumbnailView = layoutInflater.inflate(R.layout.item_image_thumbnail, gridLayout, false) android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
android.widget.FrameLayout.LayoutParams.MATCH_PARENT
val imageView = thumbnailView.findViewById<ImageView>(R.id.ivThumbnail) )
val deleteButton = thumbnailView.findViewById<View>(R.id.btnDelete) imageView.scaleType = ImageView.ScaleType.CENTER_CROP
val bitmap = BitmapFactory.decodeFile(latestFile.absolutePath)
imageView.setImageBitmap(BitmapFactory.decodeFile(file.absolutePath)) imageView.setImageBitmap(bitmap)
imageView.setOnClickListener { val deleteBtn = ImageView(this)
val intent = Intent(this@CowSelectionActivity, FullScreenImageActivity::class.java) val btnSize = (24 * resources.displayMetrics.density).toInt()
intent.putExtra("IMAGE_PATH", file.absolutePath) val btnParams = android.widget.FrameLayout.LayoutParams(btnSize, btnSize)
startActivity(intent) btnParams.gravity = android.view.Gravity.TOP or android.view.Gravity.END
} val margin = (4 * resources.displayMetrics.density).toInt()
btnParams.setMargins(margin, margin, margin, margin)
deleteButton.setOnClickListener { deleteBtn.layoutParams = btnParams
if (file.delete()) { deleteBtn.setImageResource(android.R.drawable.ic_menu_close_clear_cancel)
Toast.makeText(this@CowSelectionActivity, StringProvider.getString("toast_image_deleted"), Toast.LENGTH_SHORT).show() deleteBtn.setColorFilter(Color.RED)
refreshCowImages() deleteBtn.setBackgroundColor(Color.parseColor("#80FFFFFF"))
} else { deleteBtn.isClickable = true
Toast.makeText(this@CowSelectionActivity, StringProvider.getString("toast_error_deleting_image"), Toast.LENGTH_SHORT).show() deleteBtn.setOnClickListener {
if (latestFile.delete()) {
val remainingFiles = cowImagesFolder.listFiles()
if (remainingFiles == null || remainingFiles.isEmpty()) {
cowImagesFolder.delete()
}
refreshCowImages()
}
} }
}
gridLayout.addView(thumbnailView)
}
imagesContainer.addView(gridLayout)
}
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
}
}
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putString("COW_NAME", currentCowName) outState.putString("COW_NAME", currentCowName)
} }
} }

View File

@ -93,11 +93,19 @@ class FrameProcessor {
val rawBitmap = Bitmap.createBitmap(colors, fullWidth, fullHeight, Bitmap.Config.ARGB_8888) val rawBitmap = Bitmap.createBitmap(colors, fullWidth, fullHeight, Bitmap.Config.ARGB_8888)
// Rotate if needed // Rotate and Scale if needed
bitmapMask = if (isPortrait) { bitmapMask = if (isPortrait) {
try { try {
val matrix = Matrix() val matrix = Matrix()
// Rotate 90 degrees
matrix.postRotate(90f) 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) Bitmap.createBitmap(rawBitmap, 0, 0, rawBitmap.width, rawBitmap.height, matrix, true)
} catch (e: Exception) { } catch (e: Exception) {
Log.e("FrameProcessor", "Error rotating mask", e) Log.e("FrameProcessor", "Error rotating mask", e)
@ -127,7 +135,17 @@ class FrameProcessor {
} }
if (comparisonMask != null) { 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_EUCLIDEAN -> calculateEuclideanDistance(savedMask, comparisonMask, thresholdPercent)
HomeActivity.ALGORITHM_JACCARD -> calculateJaccardSimilarity(savedMask, comparisonMask, thresholdPercent) HomeActivity.ALGORITHM_JACCARD -> calculateJaccardSimilarity(savedMask, comparisonMask, thresholdPercent)
else -> calculateHammingDistance(savedMask, comparisonMask, thresholdPercent) else -> calculateHammingDistance(savedMask, comparisonMask, thresholdPercent)
@ -229,19 +247,13 @@ class FrameProcessor {
var union = 0 var union = 0
for (i in pixels1.indices) { for (i in pixels1.indices) {
// mask1 is Saved Mask (Inverse: Subject is Transparent/0, Background is Black/255) // mask1 is Saved Mask (Likely Inverted in StorageUtils: Subject is Transparent/0, Background is Black/255)
// NOTE: Wait, the saved mask was inverted originally? // But check how it's loaded. In CameraProcessor: loadSavedMask() -> decodeFile().
// Let's stick to generic "isSet" logic. // If we assume saved mask is inverted (subject transparent), then alpha < 128 is subject.
// 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
val alpha1 = (pixels1[i] ushr 24) and 0xFF val alpha1 = (pixels1[i] ushr 24) and 0xFF
val isSubject1 = alpha1 < 128 val isSubject1 = alpha1 < 128
// mask2 is Live Mask (Subject is Magenta/180 alpha)
val alpha2 = (pixels2[i] ushr 24) and 0xFF val alpha2 = (pixels2[i] ushr 24) and 0xFF
val isSubject2 = alpha2 > 0 val isSubject2 = alpha2 > 0

View File

@ -1,8 +1,11 @@
package com.example.animalrating package com.example.animalrating
import android.content.Intent
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import java.io.File import java.io.File
@ -12,8 +15,15 @@ class FullScreenImageActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_full_screen_image) setContentView(R.layout.activity_full_screen_image)
StringProvider.initialize(this)
val imagePath = intent.getStringExtra("IMAGE_PATH") 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) { if (imagePath != null) {
val file = File(imagePath) val file = File(imagePath)
if (file.exists()) { if (file.exists()) {
@ -21,9 +31,32 @@ class FullScreenImageActivity : AppCompatActivity() {
findViewById<ImageView>(R.id.fullScreenImageView).setImageBitmap(bitmap) findViewById<ImageView>(R.id.fullScreenImageView).setImageBitmap(bitmap)
} }
} }
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
}
findViewById<Button>(R.id.btnBack).setOnClickListener { val btnBack = findViewById<ImageButton>(R.id.btnBack)
btnBack.contentDescription = StringProvider.getString("content_desc_back")
btnBack.setOnClickListener {
finish() 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.content.res.ColorStateList
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Bundle import android.os.Bundle
import android.view.Gravity
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import java.io.File import java.io.File
import java.io.FileWriter
import java.util.Locale import java.util.Locale
class GalleryActivity : AppCompatActivity() { class GalleryActivity : AppCompatActivity() {
@ -22,17 +25,14 @@ class GalleryActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_gallery) setContentView(R.layout.activity_gallery)
// Initialize StringProvider
StringProvider.initialize(this) StringProvider.initialize(this)
// Setup Back Button
val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar) val toolbar = findViewById<androidx.appcompat.widget.Toolbar>(R.id.toolbar)
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setDisplayShowTitleEnabled(false) supportActionBar?.setDisplayShowTitleEnabled(false)
// Set Toolbar title
findViewById<TextView>(R.id.tvToolbarTitle)?.text = StringProvider.getString("title_gallery") findViewById<TextView>(R.id.tvToolbarTitle)?.text = StringProvider.getString("title_gallery")
toolbar.setNavigationOnClickListener { toolbar.setNavigationOnClickListener {
@ -52,15 +52,14 @@ class GalleryActivity : AppCompatActivity() {
private fun refreshGallery() { private fun refreshGallery() {
container.removeAllViews() 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 cowNamesFromFolders = cowFolders.map { it.name }
val groupedFiles = files?.groupBy { file ->
val parts = file.name.split("_")
if (parts.size >= 2) "${parts[0]}_${parts[1]}" else "unknown"
} ?: emptyMap()
val csvFile = File(filesDir, "cow_profiles.csv") val docsFolder = StorageUtils.getDocumentsFolder()
val csvFile = File(docsFolder, "cow_profiles.csv")
val cowDetails = if (csvFile.exists()) { val cowDetails = if (csvFile.exists()) {
csvFile.readLines().associate { line -> csvFile.readLines().associate { line ->
val parts = line.split(",") val parts = line.split(",")
@ -69,38 +68,128 @@ class GalleryActivity : AppCompatActivity() {
} else { } else {
emptyMap() emptyMap()
} }
val allCowNames = (cowDetails.keys + cowNamesFromFolders).filter { it.isNotEmpty() && it != "CowID" }.distinct()
groupedFiles.forEach { (cowName, cowFiles) -> allCowNames.forEach { cowName ->
val details = cowDetails[cowName] ?: emptyList() val details = cowDetails[cowName] ?: emptyList()
val cowImageFolder = StorageUtils.getCowImageFolder(cowName)
val cowFiles = cowImageFolder.listFiles { _, name -> name.endsWith(".jpg") }?.toList() ?: emptyList()
addCowSection(cowName, cowFiles, details) addCowSection(cowName, cowFiles, details)
} }
} }
private fun addCowSection(cowName: String, cowFiles: List<File>, details: List<String>) { private fun addCowSection(cowName: String, cowFiles: List<File>, details: List<String>) {
// Cow Name Header and Retake Button // Main Card
val headerLayout = LinearLayout(this).apply { val card = CardView(this).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams( layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT LinearLayout.LayoutParams.WRAP_CONTENT
).apply { ).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
} }
} }
val nameView = TextView(this).apply { val nameView = TextView(this).apply {
text = if (details.isNotEmpty()) "${StringProvider.getString("text_cow_id")} $cowName" else cowName text = if (details.isNotEmpty()) "${StringProvider.getString("text_cow_id")} $cowName" else cowName
textSize = 20f textSize = 20f
setTypeface(null, android.graphics.Typeface.BOLD) setTypeface(null, android.graphics.Typeface.BOLD)
setTextColor(android.graphics.Color.parseColor("#3E2723")) 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( layoutParams = LinearLayout.LayoutParams(
0, 0,
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT,
1f 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") text = StringProvider.getString("btn_edit")
textSize = 12f textSize = 12f
setTypeface(null, android.graphics.Typeface.BOLD) setTypeface(null, android.graphics.Typeface.BOLD)
@ -109,9 +198,11 @@ class GalleryActivity : AppCompatActivity() {
backgroundTintList = ColorStateList.valueOf(android.graphics.Color.parseColor("#EFEBE9")) backgroundTintList = ColorStateList.valueOf(android.graphics.Color.parseColor("#EFEBE9"))
strokeWidth = (1 * resources.displayMetrics.density).toInt() strokeWidth = (1 * resources.displayMetrics.density).toInt()
strokeColor = ColorStateList.valueOf(android.graphics.Color.parseColor("#5D4037")) strokeColor = ColorStateList.valueOf(android.graphics.Color.parseColor("#5D4037"))
layoutParams = buttonParams
insetTop = 0
insetBottom = 0
minHeight = 0 minHeight = 0
minimumHeight = 0 minimumHeight = 0
setPadding(24, 20, 24, 20)
setOnClickListener { setOnClickListener {
val intent = Intent(this@GalleryActivity, CowSelectionActivity::class.java) val intent = Intent(this@GalleryActivity, CowSelectionActivity::class.java)
@ -119,137 +210,191 @@ class GalleryActivity : AppCompatActivity() {
startActivity(intent) startActivity(intent)
} }
} }
headerLayout.addView(nameView)
headerLayout.addView(retakeButton)
container.addView(headerLayout)
// Display details if available val rateButton = MaterialButton(this).apply {
// Header: CowID,Species,Breed,Age,MilkYield,CalvingNumber,ReproductiveStatus,Description text = "Rate" // Add string if needed, skipping as per instruction to be concise unless requested
if (details.size >= 7) { textSize = 12f
val detailsLayout = LinearLayout(this).apply { setTypeface(null, android.graphics.Typeface.BOLD)
orientation = LinearLayout.VERTICAL 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
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( layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT LinearLayout.LayoutParams.WRAP_CONTENT
).apply { ).apply {
setMargins(0, 0, 0, 16) setMargins(16, 0, 16, 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"))
} }
} }
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.
infoText.append("${StringProvider.getString("label_species")} ${details.getOrElse(1) { "-" }} ") // Sort images or keep them in order. The user asked for 3 in each row.
infoText.append("${StringProvider.getString("label_breed")} ${details.getOrElse(2) { "-" }}\n") cowFiles.forEach { file ->
infoText.append("${StringProvider.getString("label_age")} ${details.getOrElse(3) { "-" }} ${StringProvider.getString("unit_years")} ") val thumbnailView = layoutInflater.inflate(R.layout.item_image_thumbnail, gridLayout, false)
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) { "-" }} ") val imageView = thumbnailView.findViewById<ImageView>(R.id.ivThumbnail)
infoText.append("${StringProvider.getString("label_status")} ${details.getOrElse(6) { "-" }}") 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() // Optionally set layout params for thumbnailView to ensure 3 per row
textSize = 14f val displayMetrics = resources.displayMetrics
setTextColor(android.graphics.Color.parseColor("#5D4037")) val screenWidth = displayMetrics.widthPixels
setLineSpacing(10f, 1f)
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) val separator = android.view.View(this).apply {
container.addView(detailsLayout) layoutParams = LinearLayout.LayoutParams(
} LinearLayout.LayoutParams.MATCH_PARENT,
2
// Group files by orientation (e.g., left, right, etc.) ).apply {
val filesByOrientation = cowFiles.groupBy { file -> setMargins(0, 16, 0, 16)
val parts = file.name.split("_") }
if (parts.size >= 3) parts[2] else "unknown" setBackgroundColor(android.graphics.Color.LTGRAY)
}
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)
} }
setBackgroundColor(android.graphics.Color.LTGRAY) container.addView(separator)
} }
container.addView(separator)
} }
private fun addOrientationSection(orientationStr: String, files: List<File>) { private fun deleteCow(cowName: String) {
// Orientation Header // Delete Images
val orientationHeader = TextView(this).apply { val imageFolder = StorageUtils.getCowImageFolder(cowName)
val key = when(orientationStr.lowercase(Locale.getDefault())) { if (imageFolder.exists()) {
"front" -> "text_front_view" imageFolder.deleteRecursively()
"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)
} }
container.addView(orientationHeader)
val docsFolder = StorageUtils.getDocumentsFolder()
// Grid for thumbnails
val gridLayout = android.widget.GridLayout(this).apply { // Delete Profile
columnCount = 3 val profileFile = File(docsFolder, "cow_profiles.csv")
layoutParams = LinearLayout.LayoutParams( if (profileFile.exists()) {
LinearLayout.LayoutParams.MATCH_PARENT, val lines = profileFile.readLines()
LinearLayout.LayoutParams.WRAP_CONTENT val newLines = lines.filter { !it.startsWith("$cowName,") }
).apply { FileWriter(profileFile).use { writer ->
setMargins(16, 0, 16, 16) newLines.forEach { writer.write(it + "\n") }
} }
} }
// Images for this orientation // Delete Ratings
files.forEach { file -> val ratingsFile = File(docsFolder, "cow_ratings.csv")
val thumbnailView = layoutInflater.inflate(R.layout.item_image_thumbnail, gridLayout, false) if (ratingsFile.exists()) {
val lines = ratingsFile.readLines()
val imageView = thumbnailView.findViewById<ImageView>(R.id.ivThumbnail) val newLines = lines.filter { !it.startsWith("$cowName,") }
val deleteButton = thumbnailView.findViewById<android.view.View>(R.id.btnDelete) FileWriter(ratingsFile).use { writer ->
newLines.forEach { writer.write(it + "\n") }
imageView.setImageBitmap(BitmapFactory.decodeFile(file.absolutePath))
imageView.setOnClickListener {
val intent = Intent(this@GalleryActivity, FullScreenImageActivity::class.java)
intent.putExtra("IMAGE_PATH", file.absolutePath)
startActivity(intent)
} }
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() { override fun onResume() {
super.onResume() super.onResume()
refreshGallery() refreshGallery()
} }
} }

View File

@ -2,20 +2,27 @@ package com.example.animalrating
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.PopupMenu import android.widget.PopupMenu
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.Spinner import android.widget.Spinner
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.cardview.widget.CardView
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat 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.button.MaterialButton
import com.google.android.material.switchmaterial.SwitchMaterial
class HomeActivity : AppCompatActivity() { class HomeActivity : AppCompatActivity() {
@ -25,7 +32,13 @@ class HomeActivity : AppCompatActivity() {
const val ALGORITHM_JACCARD = "Jaccard Similarity" const val ALGORITHM_JACCARD = "Jaccard Similarity"
private const val PERMISSION_REQUEST_CODE = 101 private const val PERMISSION_REQUEST_CODE = 101
private const val PREF_COW_ILLUSTRATION_INDEX = "COW_ILLUSTRATION_INDEX" 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -44,23 +57,27 @@ class HomeActivity : AppCompatActivity() {
permissions.add(android.Manifest.permission.CAMERA) 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) { if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissions.add(android.Manifest.permission.WRITE_EXTERNAL_STORAGE) permissions.add(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
} }
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissions.add(android.Manifest.permission.READ_EXTERNAL_STORAGE) 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()) { if (permissions.isNotEmpty()) {
@ -69,6 +86,15 @@ class HomeActivity : AppCompatActivity() {
} }
private fun setupUI() { 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 // Language Spinner
val languageSpinner = findViewById<Spinner>(R.id.spinnerLanguage) val languageSpinner = findViewById<Spinner>(R.id.spinnerLanguage)
val languages = StringProvider.getLanguages() val languages = StringProvider.getLanguages()
@ -76,7 +102,6 @@ class HomeActivity : AppCompatActivity() {
languageAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) languageAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
languageSpinner.adapter = languageAdapter languageSpinner.adapter = languageAdapter
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
val savedLang = prefs.getString("LANGUAGE", "English") val savedLang = prefs.getString("LANGUAGE", "English")
languageSpinner.setSelection(languages.indexOf(savedLang)) languageSpinner.setSelection(languages.indexOf(savedLang))
@ -99,17 +124,18 @@ class HomeActivity : AppCompatActivity() {
} }
// Set text from StringProvider // 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.btnViewGallery).text = StringProvider.getString("btn_view_gallery")
findViewById<MaterialButton>(R.id.btnSelectCow).text = StringProvider.getString("btn_select_cow") findViewById<MaterialButton>(R.id.btnSelectCow).text = StringProvider.getString("btn_select_cow")
findViewById<TextView>(R.id.tvAlgorithmLabel).text = StringProvider.getString("label_algorithm") findViewById<TextView>(R.id.tvAlgorithmLabel).text = StringProvider.getString("label_algorithm")
findViewById<TextView>(R.id.tvThresholdLabel).text = StringProvider.getString("label_match_threshold") findViewById<TextView>(R.id.tvThresholdLabel).text = StringProvider.getString("label_match_threshold")
// Cow Illustration and Logic // Cow Illustration and Logic
val ivCowIllustration = findViewById<ImageView>(R.id.ivCowIllustration) val ivCowIllustration = findViewById<ImageView>(R.id.ivCowIllustration)
val savedIndex = prefs.getInt(PREF_COW_ILLUSTRATION_INDEX, 0) val savedIndex = prefs.getInt(PREF_COW_ILLUSTRATION_INDEX, 0)
setCowIllustration(ivCowIllustration, savedIndex) setCowIllustration(ivCowIllustration, savedIndex)
ivCowIllustration.setOnClickListener { view -> ivCowIllustration.setOnClickListener { view ->
showIllustrationPopup(view, ivCowIllustration) showIllustrationPopup(view, ivCowIllustration)
} }
@ -120,28 +146,90 @@ class HomeActivity : AppCompatActivity() {
} }
findViewById<MaterialButton>(R.id.btnSelectCow).setOnClickListener { 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 // Algorithm Spinner
val spinner = findViewById<Spinner>(R.id.spinnerAlgorithm) 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) adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinner.adapter = adapter spinner.adapter = adapter
// Set default selection from preferences or intent // Set default selection from preferences or intent
val savedAlg = prefs.getString("ALGORITHM", ALGORITHM_HAMMING) 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 // Threshold SeekBar
val seekBar = findViewById<SeekBar>(R.id.seekBarThreshold) val seekBar = findViewById<SeekBar>(R.id.seekBarThreshold)
val tvThreshold = findViewById<TextView>(R.id.tvThresholdValue) val tvThreshold = findViewById<TextView>(R.id.tvThresholdValue)
val savedThreshold = prefs.getInt("THRESHOLD", 75) val savedThreshold = prefs.getInt("THRESHOLD", 75)
seekBar.progress = savedThreshold seekBar.progress = savedThreshold
tvThreshold.text = "$savedThreshold%" tvThreshold.text = "$savedThreshold%"
seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { seekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
tvThreshold.text = "$progress%" tvThreshold.text = "$progress%"
@ -149,6 +237,35 @@ class HomeActivity : AppCompatActivity() {
override fun onStartTrackingTouch(seekBar: SeekBar?) {} override fun onStartTrackingTouch(seekBar: SeekBar?) {}
override fun onStopTrackingTouch(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) { private fun showIllustrationPopup(anchor: View, imageView: ImageView) {
@ -171,48 +288,49 @@ class HomeActivity : AppCompatActivity() {
} }
private fun setCowIllustration(imageView: ImageView, index: Int) { 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 resName = "cow_illustration_$index"
val resId = resources.getIdentifier(resName, "drawable", packageName) val resId = resources.getIdentifier(resName, "drawable", packageName)
if (resId != 0) { if (resId != 0) {
imageView.setImageResource(resId) imageView.setImageResource(resId)
} else { } else {
// Fallback to default if specific one not found
if (index == 0) { 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) val defaultId = resources.getIdentifier("cow_illustration", "drawable", packageName)
if (defaultId != 0) imageView.setImageResource(defaultId) if (defaultId != 0) imageView.setImageResource(defaultId)
} }
} }
} }
private fun saveSettings() { private fun saveSettings() {
val spinner = findViewById<Spinner>(R.id.spinnerAlgorithm) val spinner = findViewById<Spinner>(R.id.spinnerAlgorithm)
val seekBar = findViewById<SeekBar>(R.id.seekBarThreshold) val seekBar = findViewById<SeekBar>(R.id.seekBarThreshold)
val showSegmentationMask = findViewById<SwitchMaterial>(R.id.switchMaskDisplay)
if (spinner != null && seekBar != null) {
val selectedAlgorithm = spinner.selectedItem?.toString() ?: ALGORITHM_HAMMING if (spinner != null && seekBar != null && showSegmentationMask != null) {
val selectedIndex = spinner.selectedItemPosition
val selectedAlgorithm = if (selectedIndex >= 0 && selectedIndex < internalAlgorithms.size) {
internalAlgorithms[selectedIndex]
} else {
ALGORITHM_HAMMING
}
val threshold = seekBar.progress val threshold = seekBar.progress
// Save to preferences // Save to preferences
val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE) val prefs = getSharedPreferences("AnimalRatingPrefs", MODE_PRIVATE)
val showMask = prefs.getBoolean(PREF_MASK_DISPLAY, false)
prefs.edit().apply { prefs.edit().apply {
putString("ALGORITHM", selectedAlgorithm) putString("ALGORITHM", selectedAlgorithm)
putInt("THRESHOLD", threshold) putInt("THRESHOLD", threshold)
putBoolean(PREF_MASK_DISPLAY, showMask)
apply() apply()
} }
} }
} }
private fun saveSettingsAndStart() { private fun saveSettingsAndStart() {
saveSettings() if(!IS_RELEASE_BUILD)
saveSettings()
startActivity(Intent(this, CowSelectionActivity::class.java)) 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 bmpW = bmp.width.toFloat()
val bmpH = bmp.height.toFloat() val bmpH = bmp.height.toFloat()
val viewAspect = viewW / viewH // Calculate scale to fit (FIT_CENTER)
val bmpAspect = bmpW / bmpH val scale = kotlin.math.min(viewW / bmpW, viewH / bmpH)
val cropRect: RectF val scaledW = bmpW * scale
val scaledH = bmpH * scale
if (bmpAspect > viewAspect) { val left = (viewW - scaledW) / 2f
// Bitmap is wider → crop left & right val top = (viewH - scaledH) / 2f
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)
}
// Scale to exactly fill screen val destRect = RectF(left, top, left + scaledW, top + scaledH)
val destRect = RectF(0f, 0f, viewW, viewH) 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"/> android:layout_marginBottom="24dp"/>
<!-- Form Fields --> <!-- 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 <com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilSpecies" android:id="@+id/tilSpecies"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -56,8 +65,7 @@
app:boxCornerRadiusTopEnd="12dp" app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp" app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp" app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41" app:boxStrokeColor="#6D4C41">
android:hint="Species">
<AutoCompleteTextView <AutoCompleteTextView
android:id="@+id/spinnerSpecies" android:id="@+id/spinnerSpecies"
@ -66,6 +74,14 @@
android:inputType="none"/> android:inputType="none"/>
</com.google.android.material.textfield.TextInputLayout> </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 <com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilBreed" android:id="@+id/tilBreed"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -76,8 +92,7 @@
app:boxCornerRadiusTopEnd="12dp" app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp" app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp" app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41" app:boxStrokeColor="#6D4C41">
android:hint="Breed">
<AutoCompleteTextView <AutoCompleteTextView
android:id="@+id/spinnerBreed" android:id="@+id/spinnerBreed"
@ -93,48 +108,82 @@
android:baselineAligned="false" android:baselineAligned="false"
android:layout_marginBottom="16dp"> android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputLayout <LinearLayout
android:id="@+id/tilAge"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" android:orientation="vertical">
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41"
android:hint="Age (Years)">
<com.google.android.material.textfield.TextInputEditText <TextView
android:id="@+id/etAge" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="number"/> style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
</com.google.android.material.textfield.TextInputLayout> app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputEditText
android:id="@+id/tilMilk" 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_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="1" android:layout_weight="1"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox" android:orientation="vertical">
app:boxCornerRadiusTopStart="12dp"
app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41"
android:hint="Milk Yield (L)">
<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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="numberDecimal"/> style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
</com.google.android.material.textfield.TextInputLayout> 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> </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 <com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilCalving" android:id="@+id/tilCalving"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -145,8 +194,7 @@
app:boxCornerRadiusTopEnd="12dp" app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp" app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp" app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41" app:boxStrokeColor="#6D4C41">
android:hint="Calving Number">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
@ -190,6 +238,14 @@
android:text="None"/> android:text="None"/>
</RadioGroup> </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 <com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilDescription" android:id="@+id/tilDescription"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -200,8 +256,7 @@
app:boxCornerRadiusTopEnd="12dp" app:boxCornerRadiusTopEnd="12dp"
app:boxCornerRadiusBottomStart="12dp" app:boxCornerRadiusBottomStart="12dp"
app:boxCornerRadiusBottomEnd="12dp" app:boxCornerRadiusBottomEnd="12dp"
app:boxStrokeColor="#6D4C41" app:boxStrokeColor="#6D4C41">
android:hint="Description">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"
@ -225,7 +280,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:columnCount="2" android:columnCount="2"
android:rowCount="3" android:rowCount="4"
android:alignmentMode="alignMargins" android:alignmentMode="alignMargins"
android:columnOrderPreserved="false" android:columnOrderPreserved="false"
android:layout_marginBottom="24dp"> android:layout_marginBottom="24dp">
@ -362,12 +417,77 @@
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </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 <androidx.cardview.widget.CardView
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="100dp" android:layout_height="100dp"
android:layout_columnWeight="1" android:layout_columnWeight="1"
android:layout_columnSpan="2"
android:layout_margin="8dp" android:layout_margin="8dp"
app:cardCornerRadius="8dp" app:cardCornerRadius="8dp"
app:cardElevation="2dp"> app:cardElevation="2dp">

View File

@ -25,4 +25,14 @@
android:scaleType="centerInside" android:scaleType="centerInside"
android:contentDescription="Back" /> 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> </RelativeLayout>

View File

@ -1,198 +1,287 @@
<?xml version="1.0" encoding="utf-8"?> <?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: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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#F7F7F7" tools:openDrawer="start">
android:fillViewport="true">
<LinearLayout <ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
android:orientation="vertical" android:background="#F7F7F7"
android:padding="24dp" android:fillViewport="true">
android:gravity="center_horizontal">
<!-- 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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:layout_marginBottom="32dp"> android:padding="24dp"
android:gravity="center_horizontal">
<!-- Add New Cow Profile Button --> <!-- Menu Button (Top Right) to Open Drawer -->
<com.google.android.material.button.MaterialButton <LinearLayout
android:id="@+id/btnSelectCow"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="72dp" android:layout_height="wrap_content"
android:text="Add New Cow Profile" android:gravity="end"
android:textSize="18sp" 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" android:textStyle="bold"
app:cornerRadius="16dp" android:textColor="#3E2723"
app:backgroundTint="#6D4C41" android:textAlignment="center"
app:icon="@android:drawable/ic_input_add" android:layout_marginBottom="8dp"/>
app:iconGravity="textStart"
app:iconPadding="12dp"
android:layout_marginBottom="16dp"
android:elevation="4dp"/>
<!-- View Saved Profiles Button --> <!-- Subtitle -->
<com.google.android.material.button.MaterialButton <TextView
android:id="@+id/btnViewGallery" android:id="@+id/tvSubtitle"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="72dp" android:layout_height="wrap_content"
android:text="View Saved Profiles" android:text="Record and manage cow information easily."
android:textSize="18sp" android:textSize="16sp"
android:textStyle="bold" android:textColor="#6D4C41"
android:textColor="#5D4037" android:textAlignment="center"
app:cornerRadius="16dp" android:layout_marginBottom="40dp"/>
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">
<!-- Main Buttons Container -->
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:padding="20dp"> android:layout_marginBottom="32dp">
<TextView <!-- Add New Cow Profile Button -->
android:layout_width="wrap_content" <com.google.android.material.button.MaterialButton
android:layout_height="wrap_content" android:id="@+id/btnSelectCow"
android:text="Settings" android:layout_width="match_parent"
android:layout_height="72dp"
android:text="Add New Cow Profile"
android:textSize="18sp" android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="#3E2723" app:cornerRadius="16dp"
android:layout_marginBottom="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 --> <!-- View Saved Profiles Button -->
<TextView <com.google.android.material.button.MaterialButton
android:id="@+id/tvAlgorithmLabel" android:id="@+id/btnViewGallery"
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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="72dp"
android:background="@drawable/bg_spinner_rounded" android:text="View Saved Profiles"
android:padding="4dp" android:textSize="18sp"
android:layout_marginBottom="20dp"> android:textStyle="bold"
<Spinner android:textColor="#5D4037"
android:id="@+id/spinnerAlgorithm" app:cornerRadius="16dp"
android:layout_width="match_parent" app:backgroundTint="#EFEBE9"
android:layout_height="48dp"/> app:strokeColor="#5D4037"
</FrameLayout> 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 <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal" android:orientation="vertical"
android:gravity="center_vertical" android:padding="20dp">
android:layout_marginBottom="8dp">
<TextView <TextView
android:id="@+id/tvThresholdLabel" android:id="@+id/tvHeaderSettings"
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_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="75%" android:text="Settings"
android:textSize="18sp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="#3E2723"/> android:textColor="#3E2723"
</LinearLayout> android:layout_marginBottom="16dp"/>
<SeekBar <!-- Algorithm Selection -->
android:id="@+id/seekBarThreshold" <TextView
android:layout_width="match_parent" android:id="@+id/tvAlgorithmLabel"
android:layout_height="wrap_content" android:layout_width="wrap_content"
android:max="100" android:layout_height="wrap_content"
android:progressTint="#6D4C41" android:text="Algorithm"
android:thumbTint="#6D4C41"/> android:textSize="14sp"
</LinearLayout> android:textColor="#5D4037"
</androidx.cardview.widget.CardView> 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> </LinearLayout>
</ScrollView>
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -13,7 +13,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<!-- ImageView to display the saved mask (Green) --> <!-- ImageView to display the saved mask (Green)-->
<ImageView <ImageView
android:id="@+id/savedMaskOverlay" android:id="@+id/savedMaskOverlay"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -21,7 +21,7 @@
android:scaleType="centerCrop" android:scaleType="centerCrop"
android:elevation="5dp"/> android:elevation="5dp"/>
<!-- ImageView to display the segmentation mask (Live) --> <!-- ImageView to display the segmentation mask (Live)-->
<ImageView <ImageView
android:id="@+id/segmentationOverlay" android:id="@+id/segmentationOverlay"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -50,5 +50,52 @@
app:iconGravity="textStart" app:iconGravity="textStart"
app:iconPadding="0dp" app:iconPadding="0dp"
android:gravity="center"/> 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> </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"> android:layout_margin="4dp">
<androidx.cardview.widget.CardView <androidx.cardview.widget.CardView
android:layout_width="100dp" android:layout_width="match_parent"
android:layout_height="100dp" android:layout_height="match_parent"
app:cardCornerRadius="8dp" app:cardCornerRadius="8dp"
app:cardElevation="2dp"> app:cardElevation="2dp">
@ -19,6 +19,18 @@
android:background="#E0E0E0"/> android:background="#E0E0E0"/>
</androidx.cardview.widget.CardView> </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 <com.google.android.material.button.MaterialButton
android:id="@+id/btnDelete" android:id="@+id/btnDelete"
@ -39,6 +51,7 @@
android:insetBottom="0dp" android:insetBottom="0dp"
app:iconGravity="textStart" app:iconGravity="textStart"
app:iconPadding="0dp" app:iconPadding="0dp"
android:gravity="center"/> android:gravity="center"
android:elevation="6dp"/>
</FrameLayout> </FrameLayout>

View File

@ -5,15 +5,34 @@
"title_gallery": "Saved Cow Profiles", "title_gallery": "Saved Cow Profiles",
"title_add_cow_details": "Add Cow Details", "title_add_cow_details": "Add Cow Details",
"title_cow_selection": "Cow Selection", "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_view_gallery": "View Saved Cow Profiles",
"btn_select_cow": "Select a Cow", "btn_select_cow": "Add New Cow Profile",
"label_algorithm": "Algorithm", "label_algorithm": "Algorithm",
"label_match_threshold": "Match Threshold", "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_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_search_profiles": "Search profiles...",
"hint_comments": "Comments",
"toast_image_deleted": "Image deleted", "toast_image_deleted": "Image deleted",
"toast_error_deleting_image": "Error deleting image", "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_cow_id": "Cow ID:",
"text_no_details": "No details available",
"label_species": "Species:", "label_species": "Species:",
"label_breed": "Breed:", "label_breed": "Breed:",
"label_age": "Age:", "label_age": "Age:",
@ -38,6 +57,8 @@
"text_left_side": "Left Side", "text_left_side": "Left Side",
"text_right_side": "Right Side", "text_right_side": "Right Side",
"text_angle_view": "Angle View", "text_angle_view": "Angle View",
"text_left_angle": "Left Angle",
"text_right_angle": "Right Angle",
"btn_save_profile": "Save Profile", "btn_save_profile": "Save Profile",
"btn_cancel": "Cancel", "btn_cancel": "Cancel",
"toast_profile_saved": "Profile Saved!", "toast_profile_saved": "Profile Saved!",
@ -55,7 +76,33 @@
"breed_gir": "Gir", "breed_gir": "Gir",
"breed_red_sindhi": "Red Sindhi", "breed_red_sindhi": "Red Sindhi",
"breed_murrah": "Murrah", "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": { "Hindi": {
"app_name": "पशु रेटिंग", "app_name": "पशु रेटिंग",
@ -63,15 +110,34 @@
"title_gallery": "सहेजे गए गाय प्रोफाइल", "title_gallery": "सहेजे गए गाय प्रोफाइल",
"title_add_cow_details": "गाय का विवरण जोड़ें", "title_add_cow_details": "गाय का विवरण जोड़ें",
"title_cow_selection": "गाय का चयन", "title_cow_selection": "गाय का चयन",
"title_rate_cow": "गाय का मूल्यांकन",
"header_photos": "तस्वीरें",
"header_cow_details": "गाय का विवरण",
"header_feature_ratings": "विशेषता रेटिंग",
"header_settings": "सेटिंग्स",
"subtitle_home": "गाय की जानकारी आसानी से रिकॉर्ड और प्रबंधित करें।",
"btn_view_gallery": "सहेजे गए प्रोफाइल देखें", "btn_view_gallery": "सहेजे गए प्रोफाइल देखें",
"btn_select_cow": "एक गाय का चयन करें", "btn_select_cow": "नई गाय प्रोफ़ाइल जोड़ें",
"label_algorithm": "एल्गोरिदम", "label_algorithm": "एल्गोरिदम",
"label_match_threshold": "मैच थ्रेसहोल्ड", "label_match_threshold": "मैच थ्रेसहोल्ड",
"label_auto_capture": "ऑटो कैप्चर",
"label_enable_debug": "डिबग सक्षम करें",
"label_mask_display": "सेगमेंटेशन मास्क दिखाएं",
"btn_edit": "संपादित करें", "btn_edit": "संपादित करें",
"btn_rate": "रेट करें",
"btn_delete": "हटाएँ",
"btn_save_rating": "रेटिंग सहेजें",
"btn_retake": "फोटो फिर से लें",
"btn_exit": "बाहर जाएं",
"hint_search_profiles": "प्रोफ़ाइल खोजें...", "hint_search_profiles": "प्रोफ़ाइल खोजें...",
"hint_comments": "टिप्पणियाँ",
"toast_image_deleted": "छवि हटा दी गई", "toast_image_deleted": "छवि हटा दी गई",
"toast_error_deleting_image": "छवि हटाने में त्रुटि", "toast_error_deleting_image": "छवि हटाने में त्रुटि",
"toast_ratings_saved": "रेटिंग सहेजी गई!",
"toast_error_saving_ratings": "रेटिंग सहेजने में त्रुटि",
"toast_profile_deleted": "गाय प्रोफ़ाइल हटाई गई",
"text_cow_id": "गाय आईडी:", "text_cow_id": "गाय आईडी:",
"text_no_details": "कोई विवरण उपलब्ध नहीं",
"label_species": "प्रजाति:", "label_species": "प्रजाति:",
"label_breed": "नस्ल:", "label_breed": "नस्ल:",
"label_age": "आयु:", "label_age": "आयु:",
@ -96,6 +162,8 @@
"text_left_side": "बाईं ओर", "text_left_side": "बाईं ओर",
"text_right_side": "दाईं ओर", "text_right_side": "दाईं ओर",
"text_angle_view": "कोणीय दृश्य", "text_angle_view": "कोणीय दृश्य",
"text_left_angle": "बायां कोण",
"text_right_angle": "दायां कोण",
"btn_save_profile": "प्रोफ़ाइल सहेजें", "btn_save_profile": "प्रोफ़ाइल सहेजें",
"btn_cancel": "रद्द करें", "btn_cancel": "रद्द करें",
"toast_profile_saved": "प्रोफ़ाइल सहेजा गया!", "toast_profile_saved": "प्रोफ़ाइल सहेजा गया!",
@ -113,6 +181,32 @@
"breed_gir": "गिर", "breed_gir": "गिर",
"breed_red_sindhi": "लाल सिंधी", "breed_red_sindhi": "लाल सिंधी",
"breed_murrah": "मुर्रा", "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": "जैकार्ड समानता"
} }
} }