From 328c763fbc2dc332ece19d58ae3f8a7c4b7fd324 Mon Sep 17 00:00:00 2001 From: Pedro Javier Date: Fri, 26 Jun 2026 09:35:39 +0200 Subject: [PATCH] =?UTF-8?q?"Implementaci=C3=B3n=20de=20OCR,=20historial=20?= =?UTF-8?q?filtrado=20y=20predicci=C3=B3n=20de=20mantenimiento=20con=20mar?= =?UTF-8?q?gen=20de=20viaje."=20Y=20si=20quieres=20ser=20m=C3=A1s=20detall?= =?UTF-8?q?ado,=20puedes=20a=C3=B1adir=20debajo:=20=E2=80=A2=20Captura=20d?= =?UTF-8?q?e=20kms=20con=20Google=20ML=20Kit.=20=E2=80=A2=20Conexi=C3=B3n?= =?UTF-8?q?=20directa=20a=20MariaDB=20remota.=20=E2=80=A2=20C=C3=A1lculo?= =?UTF-8?q?=20de=20cambio=20de=20aceite=20con=20margen=20de=20320km=20para?= =?UTF-8?q?=20viaje=20a=20Puertollano.=20=E2=80=A2=20A=C3=B1adido=20histor?= =?UTF-8?q?ial=20con=20filtros=20y=20registro=20de=20otros=20mantenimiento?= =?UTF-8?q?s=20(neum=C3=A1ticos).=20=E2=80=A2=20Creado=20archivo=20README.?= =?UTF-8?q?md=20con=20la=20documentaci=C3=B3n=20del=20proyecto.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 41 ++ .../com/example/kilometrapp/MainActivity.kt | 355 +++++++++++++----- app/src/main/res/layout/activity_main.xml | 38 +- app/src/main/res/menu/main_menu.xml | 11 + app/src/main/res/values/strings.xml | 43 +++ 5 files changed, 382 insertions(+), 106 deletions(-) create mode 100644 README.md create mode 100644 app/src/main/res/menu/main_menu.xml diff --git a/README.md b/README.md new file mode 100644 index 0000000..7949690 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# Mantenimiento (Kilometrapp) - Estado Actual del Proyecto + +Aplicación Android nativa desarrollada en Kotlin diseñada para la gestión logística y mecánica de vehículos mediante captura inteligente de kilometraje. + +### 🚀 Características Implementadas: + +1. **Captura Inteligente (OCR):** + * Integración de **Google ML Kit Text Recognition** para la lectura automática del cuentakilómetros. + * Uso de **CameraX** con implementación de recuadro de enfoque (*Focus Frame*) y soporte para **Pinch-to-Zoom (1x - 8x)**. + * Algoritmo de limpieza de cadenas mediante Regex para separar valores pegados (ej: temperatura de kilometraje). + +2. **Persistencia en la Nube:** + * Conexión directa a base de datos **MariaDB** remota. + * Uso del driver **MySQL Connector 5.1.49** para garantizar máxima compatibilidad con dispositivos Android (evitando errores de `DriverAction`). + * Sistema de guardado seguro mediante *Executors* para no bloquear la interfaz de usuario. + +3. **Lógica de Negocio y Predicción:** + * **Algoritmo Predictivo de Aceite:** Cálculo automático del próximo cambio (intervalo de 15,000 km). + * **Margen de Viaje Geográfico:** Resta automática de **320 km** al cálculo de mantenimiento para planificar el viaje de retorno Torremolinos - Puertollano. + * **Estimación Temporal:** Cálculo de fecha aproximada de mantenimiento basada en la media de kilómetros diarios recorridos. + +4. **Gestión de Historial y UX:** + * **Toolbar con Menú:** Acceso directo a registros históricos desde la barra superior. + * **Filtros Inteligentes:** Buscador de registros por categorías (Revisiones, Aceite, Neumáticos) mediante consultas `LIKE` flexibles. + * **Registro Multimodal:** Opciones rápidas para mantenimiento común y entrada de texto libre para incidencias personalizadas. + * **Localización:** Interfaz y fechas totalmente adaptadas al formato español (`dd/MM/yyyy`). + +### 🛠️ Stack Tecnológico: +* **Lenguaje:** Kotlin +* **Cámara:** CameraX (Core, Lifecycle, View) +* **IA/ML:** ML Kit Text Recognition (Latin) +* **Base de Datos:** MariaDB / JDBC (MySQL Connector) +* **UI:** Material Design / ConstraintLayout + +### 📊 Estructura de Datos (MariaDB): +* **Tabla:** `mantenimiento` +* **Columnas:** `id` (INT), `kms` (INT), `observaciones` (TEXT), `fecha` (DATETIME). + +--- + +**Estado:** Estable y funcional para el seguimiento diario del vehículo con margen de seguridad para viajes de larga distancia. diff --git a/app/src/main/java/com/example/kilometrapp/MainActivity.kt b/app/src/main/java/com/example/kilometrapp/MainActivity.kt index 684c6f2..0bcfd81 100644 --- a/app/src/main/java/com/example/kilometrapp/MainActivity.kt +++ b/app/src/main/java/com/example/kilometrapp/MainActivity.kt @@ -1,9 +1,13 @@ -package com.example.kilometrapp // <-- Asegúrate de que este es tu paquete exacto +package com.example.kilometrapp import android.Manifest import android.content.pm.PackageManager import android.graphics.Rect +import android.view.Menu +import android.view.MenuItem import android.view.ScaleGestureDetector +import android.view.ViewGroup +import android.widget.FrameLayout import android.os.Bundle import android.util.Log import android.widget.Button @@ -13,6 +17,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.OptIn import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar import androidx.camera.core.* import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.view.PreviewView @@ -21,6 +26,8 @@ import com.google.mlkit.vision.common.InputImage import com.google.mlkit.vision.text.Text import com.google.mlkit.vision.text.TextRecognition import com.google.mlkit.vision.text.latin.TextRecognizerOptions +import java.text.SimpleDateFormat +import java.util.* import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -29,24 +36,28 @@ class MainActivity : AppCompatActivity() { private lateinit var viewFinder: PreviewView private lateinit var overlayFrame: android.view.View private lateinit var tvResult: TextView + private lateinit var tvPrediction: TextView private lateinit var btnCapture: Button private lateinit var cameraExecutor: ExecutorService private var imageCapture: ImageCapture? = null private var cameraControl: CameraControl? = null private var currentZoomRatio = 1f - private var isDialogShowing = false + + private val dbUrl = "jdbc:mysql://mariadb.peta9.com:3306/mantenimiento_db" + private val dbUser = "pedro" + private val dbPass = "Pedro@110387" // Inicializa el OCR con las opciones estándar private val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS) - // Lanzador para solicitar el permiso de forma nativa en pantalla + // Lanzador para solicitar el permiso private val requestPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> if (isGranted) { viewFinder.post { startCamera() } } else { - Toast.makeText(this, "Permiso de cámara obligatorio denegado.", Toast.LENGTH_LONG).show() + Toast.makeText(this, getString(R.string.permission_denied), Toast.LENGTH_LONG).show() } } @@ -55,20 +66,26 @@ class MainActivity : AppCompatActivity() { try { setContentView(R.layout.activity_main) + val toolbar: Toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + viewFinder = findViewById(R.id.viewFinder) overlayFrame = findViewById(R.id.overlayFrame) tvResult = findViewById(R.id.tvResult) + tvPrediction = findViewById(R.id.tvPrediction) btnCapture = findViewById(R.id.btnCapture) cameraExecutor = Executors.newSingleThreadExecutor() + // Cargar predicción inicial + obtenerPrediccion() + // Configurar el detector de gestos de zoom val scaleGestureDetector = ScaleGestureDetector(this, object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { cameraControl?.let { control -> val scale = detector.scaleFactor currentZoomRatio *= scale - // Limitamos el zoom entre 1x y el máximo de la cámara (ej: 8x) currentZoomRatio = currentZoomRatio.coerceIn(1f, 8f) control.setZoomRatio(currentZoomRatio) } @@ -76,7 +93,6 @@ class MainActivity : AppCompatActivity() { } }) - // Aplicar el detector al visor de la cámara viewFinder.setOnTouchListener { view, event -> scaleGestureDetector.onTouchEvent(event) view.performClick() @@ -91,62 +107,55 @@ class MainActivity : AppCompatActivity() { requestPermissionLauncher.launch(Manifest.permission.CAMERA) } } catch (e: Exception) { - Log.e("Kilometrapp", "Error controlado en UI: ${e.message}") - Toast.makeText(this, "Error en la interfaz: ${e.message}", Toast.LENGTH_LONG).show() + Log.e("Kilometrapp", "Error en UI: ${e.message}") + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.main_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return if (item.itemId == R.id.action_history) { + mostrarHistorial() + true + } else { + super.onOptionsItemSelected(item) } } private fun startCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) - cameraProviderFuture.addListener({ try { val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() - - val preview = Preview.Builder() - .build() - .also { - it.surfaceProvider = viewFinder.surfaceProvider - } - + val preview = Preview.Builder().build().also { + it.surfaceProvider = viewFinder.surfaceProvider + } imageCapture = ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .build() - val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA - cameraProvider.unbindAll() - - val camera = cameraProvider.bindToLifecycle( - this, cameraSelector, preview, imageCapture - ) - + val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) cameraControl = camera.cameraControl - - // Opcional: Establecer un zoom inicial (ej: 2x) para ver mejor el tablero - // cameraControl?.setLinearZoom(0.3f) // 0.0 a 1.0 - - Log.d("Kilometrapp", "Cámara vinculada correctamente para captura") - } catch (exc: Exception) { - Log.e("Kilometrapp", "Error al inicializar CameraX", exc) - Toast.makeText(this, "Error al abrir la cámara", Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.camera_error), Toast.LENGTH_SHORT).show() } }, ContextCompat.getMainExecutor(this)) } private fun takePhoto() { val imageCapture = imageCapture ?: return - imageCapture.takePicture( ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageCapturedCallback() { override fun onCaptureSuccess(imageProxy: ImageProxy) { processStaticImage(imageProxy) } - override fun onError(exception: ImageCaptureException) { - Log.e("Kilometrapp", "Error al capturar foto: ${exception.message}") + Log.e("Kilometrapp", "Error captura: ${exception.message}") } } ) @@ -157,22 +166,18 @@ class MainActivity : AppCompatActivity() { val mediaImage = imageProxy.image if (mediaImage != null) { val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) - recognizer.process(image) .addOnSuccessListener { visionText -> val bloquesEnCuadro = visionText.textBlocks.filter { isInsideFocusFrame(it.boundingBox, imageProxy.width, imageProxy.height) } - if (bloquesEnCuadro.isEmpty()) { - Toast.makeText(this, "No se detectó texto en el recuadro", Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.no_text_detected), Toast.LENGTH_SHORT).show() } else { showSelectionDialog(bloquesEnCuadro) } } - .addOnCompleteListener { - imageProxy.close() - } + .addOnCompleteListener { imageProxy.close() } } else { imageProxy.close() } @@ -180,20 +185,13 @@ class MainActivity : AppCompatActivity() { private fun showSelectionDialog(bloques: List) { val candidatos = mutableListOf() - + val regex = Regex("\\d{2,8}") for (bloque in bloques) { - // Buscamos cualquier secuencia de números (de 2 a 8 cifras) - // Esto separará el "22" del "246115" aunque el OCR los pegue - val matches = Regex("\\d{2,8}").findAll(bloque.text) - matches.forEach { match -> + regex.findAll(bloque.text).forEach { match -> val num = match.value - // Si el número es muy largo (ej: 22246115), intentamos ver si los últimos 6 son el kilometraje if (num.length >= 7) { - val ultimosSeis = num.takeLast(6) - if (!candidatos.contains(ultimosSeis)) candidatos.add(ultimosSeis) - - val primerosDos = num.take(2) - if (!candidatos.contains(primerosDos)) candidatos.add(primerosDos) + val last6 = num.takeLast(6) + if (!candidatos.contains(last6)) candidatos.add(last6) } else { if (!candidatos.contains(num)) candidatos.add(num) } @@ -201,73 +199,243 @@ class MainActivity : AppCompatActivity() { } if (candidatos.isEmpty()) { - Toast.makeText(this, "No se encontraron números claros", Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.no_numbers_found), Toast.LENGTH_SHORT).show() return } - // Ordenamos para que los números de 6 cifras (kilometraje habitual) salgan arriba val listaFinal = candidatos.sortedByDescending { it.length }.toTypedArray() - AlertDialog.Builder(this) - .setTitle("Selecciona el Kilometraje") + .setTitle(getString(R.string.select_km_title)) .setItems(listaFinal) { _, which -> val seleccionado = listaFinal[which] - tvResult.text = "$seleccionado KM" + tvResult.text = getString(R.string.km_suffix, seleccionado) showConfirmationDialog(seleccionado) } - .setNegativeButton("Cancelar", null) + .setNegativeButton(getString(R.string.btn_cancel), null) .show() } private fun showConfirmationDialog(kilometraje: String) { - val opciones = arrayOf("Revisión kilómetros", "Cambio de aceite") - + val opciones = arrayOf( + getString(R.string.btn_revision), + getString(R.string.btn_oil_change), + getString(R.string.btn_tires), + getString(R.string.btn_other) + ) + AlertDialog.Builder(this) - .setTitle("¿Qué quieres registrar?") - .setMessage("Kilometraje: $kilometraje KM") - .setPositiveButton("Revisión") { _, _ -> - enviarAServidor(kilometraje, "Revision kilometros") + .setTitle(getString(R.string.dialog_km_message, kilometraje)) + .setItems(opciones) { _, which -> + when (which) { + 0 -> enviarAServidor(kilometraje, getString(R.string.db_obs_revision)) + 1 -> enviarAServidor(kilometraje, getString(R.string.db_obs_oil_change)) + 2 -> showCustomInputDialog(kilometraje, "Neumáticos") + 3 -> showCustomInputDialog(kilometraje, "") + } } - .setNeutralButton("Cambio Aceite") { _, _ -> - enviarAServidor(kilometraje, "Cambio de aceite") + .setNegativeButton(getString(R.string.btn_retry), null) + .show() + } + + private fun showCustomInputDialog(kilometraje: String, prefill: String) { + val input = android.widget.EditText(this) + input.hint = getString(R.string.dialog_custom_hint) + if (prefill.isNotEmpty()) { + input.setText(prefill) + input.setSelection(input.text.length) + } + + val container = FrameLayout(this) + val params = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.setMargins(60, 20, 60, 0) + input.layoutParams = params + container.addView(input) + + val title = if (prefill.isEmpty()) getString(R.string.btn_other) else getString(R.string.btn_tires) + + AlertDialog.Builder(this) + .setTitle(title) + .setView(container) + .setPositiveButton(getString(R.string.btn_save)) { _, _ -> + val textoPersonalizado = input.text.toString() + if (textoPersonalizado.isNotBlank()) { + enviarAServidor(kilometraje, textoPersonalizado) + } else { + Toast.makeText(this, "Debes escribir algo", Toast.LENGTH_SHORT).show() + } } - .setNegativeButton("Reintentar", null) + .setNegativeButton(getString(R.string.btn_cancel), null) .show() } private fun enviarAServidor(kilometraje: String, observacionesText: String) { cameraExecutor.execute { try { - // Usamos el driver de MySQL que es el más estable en Android Class.forName("com.mysql.jdbc.Driver") - - // Datos de conexión - val url = "jdbc:mysql://mariadb.peta9.com:3306/mantenimiento_db" - val user = "pedro" - val pass = "Pedro@110387" - - val connection = java.sql.DriverManager.getConnection(url, user, pass) - - // Preparar el INSERT (corregido: comas y parámetros) - val sql = "INSERT INTO mantenimiento (kms, observaciones, fecha, foto) VALUES (?, ?, NOW(), null)" + val connection = java.sql.DriverManager.getConnection(dbUrl, dbUser, dbPass) + val sql = "INSERT INTO mantenimiento (kms, observaciones, fecha) VALUES (?, ?, NOW())" val statement = connection.prepareStatement(sql) - - // Extraemos solo los números para guardar como entero val soloNumeros = kilometraje.filter { it.isDigit() }.toIntOrNull() ?: 0 - statement.setInt(1, soloNumeros) statement.setString(2, observacionesText) - statement.executeUpdate() connection.close() runOnUiThread { - Toast.makeText(this, "¡$observacionesText guardado!", Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.save_success, observacionesText), Toast.LENGTH_SHORT).show() + obtenerPrediccion() } } catch (e: Throwable) { - Log.e("Kilometrapp", "Error MariaDB: ${e.message}") runOnUiThread { - Toast.makeText(this, "Error de conexión: ${e.message}", Toast.LENGTH_LONG).show() + Toast.makeText(this, getString(R.string.connection_error, e.message), Toast.LENGTH_LONG).show() + } + } + } + } + + private fun obtenerPrediccion() { + cameraExecutor.execute { + try { + Class.forName("com.mysql.jdbc.Driver") + val connection = java.sql.DriverManager.getConnection(dbUrl, dbUser, dbPass) + + val rsOil = connection.createStatement().executeQuery( + "SELECT kms FROM mantenimiento WHERE observaciones LIKE '%aceite%' ORDER BY fecha DESC LIMIT 1" + ) + var ultimoCambio = 0 + if (rsOil.next()) ultimoCambio = rsOil.getInt("kms") + + val rsStats = connection.createStatement().executeQuery( + "SELECT MIN(fecha) as f_min, MIN(kms) as k_min, MAX(fecha) as f_max, MAX(kms) as k_max FROM mantenimiento" + ) + + var fechaEstimada: String? = null + var restan = 0 + + if (rsStats.next() && ultimoCambio > 0) { + val fMin = rsStats.getTimestamp("f_min") + val kMin = rsStats.getInt("k_min") + val fMax = rsStats.getTimestamp("f_max") + val kMax = rsStats.getInt("k_max") + + if (fMin != null && fMax != null && kMax > kMin) { + val diffMs = fMax.time - fMin.time + val diffDays = diffMs / (1000 * 60 * 60 * 24) + + if (diffDays > 0) { + val kmPorDia = (kMax - kMin).toDouble() / diffDays + val distanciaPuertollano = 320 + restan = (ultimoCambio + 15000 - distanciaPuertollano) - kMax + + if (restan > 0 && kmPorDia > 0) { + val diasFaltan = (restan / kmPorDia).toLong() + val calendar = Calendar.getInstance() + calendar.add(Calendar.DAY_OF_YEAR, diasFaltan.toInt()) + fechaEstimada = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()).format(calendar.time) + } + } + } + if (restan <= 0) { + val distanciaPuertollano = 320 + restan = (ultimoCambio + 15000 - distanciaPuertollano) - kMax + } + } + + connection.close() + + runOnUiThread { + if (ultimoCambio > 0) { + if (restan <= 0) { + tvPrediction.text = getString(R.string.urgent_oil_change, Math.abs(restan)) + tvPrediction.setTextColor(android.graphics.Color.RED) + } else { + val fechaTexto = fechaEstimada ?: "---" + tvPrediction.text = getString(R.string.next_oil_change, restan, fechaTexto) + tvPrediction.setTextColor(android.graphics.Color.parseColor("#4CAF50")) + } + } else { + tvPrediction.text = getString(R.string.no_oil_change_data) + } + } + } catch (e: Throwable) { + Log.e("Kilometrapp", "Error predicción: ${e.message}") + } + } + } + + private fun mostrarHistorial() { + val opcionesFiltro = arrayOf("Todos los registros", "Solo Revisiones", "Solo Cambios de Aceite", "Solo Neumáticos/Ruedas") + + AlertDialog.Builder(this) + .setTitle("Filtrar historial") + .setItems(opcionesFiltro) { _, which -> + when (which) { + 0 -> cargarDatosHistorial(null) + 1 -> cargarDatosHistorial(getString(R.string.db_obs_revision)) + 2 -> cargarDatosHistorial(getString(R.string.db_obs_oil_change)) + 3 -> cargarDatosHistorial("rueda") + } + } + .setNegativeButton("Cancelar", null) + .show() + } + + private fun cargarDatosHistorial(filtro: String?) { + cameraExecutor.execute { + try { + Class.forName("com.mysql.jdbc.Driver") + val connection = java.sql.DriverManager.getConnection(dbUrl, dbUser, dbPass) + + val query = if (filtro == null) { + "SELECT kms, observaciones, fecha FROM mantenimiento ORDER BY fecha DESC LIMIT 20" + } else { + val searchKey = when { + filtro.contains("aceite", ignoreCase = true) -> "aceite" + filtro.contains("rueda", ignoreCase = true) || filtro.contains("neumatico", ignoreCase = true) -> "rueda" + else -> "revision" + } + "SELECT kms, observaciones, fecha FROM mantenimiento WHERE observaciones LIKE '%$searchKey%' ORDER BY fecha DESC LIMIT 20" + } + + val rs = connection.createStatement().executeQuery(query) + + val dbFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) + val outFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val historial = mutableListOf() + + while (rs.next()) { + val km = rs.getInt("kms") + val obs = rs.getString("observaciones") + val fechaStr = rs.getString("fecha") + + val fechaFormateada = try { + val date = dbFormat.parse(fechaStr) + if (date != null) outFormat.format(date) else "---" + } catch (e: Exception) { + fechaStr?.split(".")?.get(0) ?: "---" + } + + historial.add("$km KM - $obs\n($fechaFormateada)") + } + connection.close() + + runOnUiThread { + if (historial.isEmpty()) { + Toast.makeText(this, "No hay registros para este filtro", Toast.LENGTH_SHORT).show() + } else { + AlertDialog.Builder(this) + .setTitle(getString(R.string.history_title)) + .setItems(historial.toTypedArray(), null) + .setPositiveButton("Cerrar", null) + .show() + } + } + } catch (e: Throwable) { + runOnUiThread { + Toast.makeText(this, getString(R.string.connection_error, e.message), Toast.LENGTH_LONG).show() } } } @@ -275,27 +443,14 @@ class MainActivity : AppCompatActivity() { private fun isInsideFocusFrame(boundingBox: Rect?, imgWidth: Int, imgHeight: Int): Boolean { if (boundingBox == null) return false - - // 1. Obtener dimensiones del visor y del recuadro en pantalla val viewWidth = viewFinder.width val viewHeight = viewFinder.height - - // 2. Coordenadas del recuadro de enfoque en la pantalla - val frameLeft = overlayFrame.left.toFloat() - val frameTop = overlayFrame.top.toFloat() - val frameRight = overlayFrame.right.toFloat() - val frameBottom = overlayFrame.bottom.toFloat() - - // 3. Mapear coordenadas del OCR (que vienen en tamaño de imagen) a coordenadas de pantalla - // Nota: Esto es una simplificación, pero para un recuadro central funciona bien. - val scaleX = viewWidth.toFloat() / imgHeight.toFloat() // CameraX suele invertir W/H por rotación + val scaleX = viewWidth.toFloat() / imgHeight.toFloat() val scaleY = viewHeight.toFloat() / imgWidth.toFloat() - val rectCenterX = boundingBox.centerX() * scaleX val rectCenterY = boundingBox.centerY() * scaleY - - // 4. Verificar si el centro del texto detectado está dentro del rectángulo rojo - return rectCenterX in frameLeft..frameRight && rectCenterY in frameTop..frameBottom + return rectCenterX in overlayFrame.left.toFloat()..overlayFrame.right.toFloat() && + rectCenterY in overlayFrame.top.toFloat()..overlayFrame.bottom.toFloat() } override fun onDestroy() { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index b01e349..0243ee2 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -7,12 +7,21 @@ android:layout_height="match_parent" tools:context=".MainActivity"> + + @@ -32,7 +41,7 @@ android:id="@+id/tvInstrucciones" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="ENCUADRA Y PULSA CAPTURAR" + android:text="@string/instruction_text" android:textColor="#FF0000" android:textSize="12sp" android:textStyle="bold" @@ -45,13 +54,13 @@ android:id="@+id/btnCapture" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="CAPTURAR" + android:text="@string/btn_capture" android:layout_marginBottom="16dp" app:layout_constraintBottom_toBottomOf="@id/viewFinder" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" /> - + @@ -75,12 +84,29 @@ android:id="@+id/tvResult" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Buscando cuentakilómetros..." + android:text="@string/status_searching" android:textColor="#FFFFFF" android:textSize="22sp" android:textStyle="bold" android:layout_marginTop="8dp" android:layout_gravity="center_horizontal" /> + + + + diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml new file mode 100644 index 0000000..b33871c --- /dev/null +++ b/app/src/main/res/menu/main_menu.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2208ed4..f5611c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,46 @@ Mantenimiento + + + ENCUADRA Y PULSA CAPTURAR + CAPTURAR + Kilometraje Detectado: + Buscando cuentakilómetros... + Cargando predicción... + + + Permiso de cámara obligatorio denegado. + Error al abrir la cámara + No se detectó texto en el recuadro + Selecciona el Kilometraje + No se encontraron números claros + %1$s KM + ¿Qué quieres registrar? + Kilometraje: %1$s KM + Revisión + Cambio Aceite + Reintentar + Cancelar + ¡%1$s guardado! + Error de conexión: %1$s + + + ¡URGENTE! Sal hacia Puertollano (Exceso: %1$d km) + Kms para ir a Puertollano: %1$d KM (Aprox. %2$s) + Sin registro de cambio de aceite + + + Revision kilometros + Cambio de aceite + + + Ver Historial + Últimos Registros + + + Neumáticos + Otros + Detalle del mantenimiento + Escribe aquí (ej: Pastillas, Filtros...) + Guardar \ No newline at end of file