{"id":11757,"date":"2026-04-13T10:30:00","date_gmt":"2026-04-13T08:30:00","guid":{"rendered":"https:\/\/bitacora.eniac2000.com\/?p=11757"},"modified":"2026-04-12T16:59:49","modified_gmt":"2026-04-12T14:59:49","slug":"aplicacion-android-para-fotogrametria-con-ia-fase-6-resultados-en-el-mapa-gestion-multi-bateria-y-reanudacion-de-misiones","status":"publish","type":"post","link":"https:\/\/bitacora.eniac2000.com\/?p=11757","title":{"rendered":"Aplicaci\u00f3n Android para fotogrametr\u00eda con IA. Fase 6: Resultados en el mapa, gesti\u00f3n multi-bater\u00eda y reanudaci\u00f3n de misiones"},"content":{"rendered":"<div class=\"seriesmeta\">Esta entrada es la parte 9 de 9 de la serie <a href=\"https:\/\/bitacora.eniac2000.com\/?series=fotogrametria-asistida-por-ia\" class=\"series-1957\" title=\"Fotogrametr\u00eda asistida por IA\">Fotogrametr\u00eda asistida por IA<\/a><\/div>\n<p>Hasta la <a href=\"https:\/\/bitacora.eniac2000.com\/?p=11703\">Fase 5<\/a>, la aplicaci\u00f3n era capaz de planificar y ejecutar misiones, enviar las im\u00e1genes a procesar y auditar el vuelo en tiempo real. Lo que le faltaba era cerrar el otro extremo del ciclo: mostrar los resultados dentro de la <em>app<\/em>, sin tener que abrir el navegador para ver el ortomosaico o los hallazgos IA. Y, en paralelo, dos limitaciones pr\u00e1cticas que cualquiera que haya hecho fotogrametr\u00eda de campo conoce bien: qu\u00e9 pasa cuando la bater\u00eda se acaba a mitad de misi\u00f3n, y c\u00f3mo se contin\u00faa desde donde se dej\u00f3. La Fase 6 resuelve ambas cosas.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">El ortomosaico como capa en el mapa<\/h2>\n\n\n\n<p>El resultado m\u00e1s visible de esta fase es poder ver el ortomosaico generado por ODM superpuesto directamente sobre el mapa de la aplicaci\u00f3n. La implementaci\u00f3n usa el servidor de <em>tiles<\/em> que WebODM expone para cada tarea procesada: una vez que el procesado termina, WebODM sirve el ortomosaico como <em>tiles<\/em> en coordenadas XYZ est\u00e1ndar, que MapLibre puede consumir como fuente <em>raster<\/em>.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"461\" src=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/Screenshot_20260411-175215-1024x461.png\" alt=\"Visualizaci\u00f3n del ortomosaico sobre el mapa en la app. as\u00ed como la trayectoria del dron y los hallazgos observados con IA\" class=\"wp-image-11746\" srcset=\"https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/Screenshot_20260411-175215-1024x461.png 1024w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/Screenshot_20260411-175215-300x135.png 300w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/Screenshot_20260411-175215-768x346.png 768w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/Screenshot_20260411-175215-1536x691.png 1536w, https:\/\/bitacora.eniac2000.com\/wp-content\/uploads\/2026\/04\/Screenshot_20260411-175215-2048x922.png 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Visualizaci\u00f3n del ortomosaico sobre el mapa en la app. as\u00ed como la trayectoria del dron y los hallazgos observados con IA<\/figcaption><\/figure>\n\n\n\n<p>La URL de los <em>tiles<\/em> tiene esta forma:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{serverUrl}\/projects\/{projectId}\/tasks\/{taskId}\/orthophoto\/tiles\/{z}\/{x}\/{y}.png?jwt={token}<\/code><\/pre>\n\n\n\n<p>La autenticaci\u00f3n JWT se renueva autom\u00e1ticamente antes de construir la URL, para evitar que los <em>tiles<\/em> fallen si el <em>token<\/em> ha caducado desde la \u00faltima sesi\u00f3n. El <em>toggle<\/em> \u00abOrtomosaico ODM\u00bb solo aparece en el selector de capas cuando la misi\u00f3n tiene un proyecto y tarea WebODM asociados, as\u00ed que no ocupa espacio en la interfaz hasta que hay algo que mostrar.<\/p>\n\n\n\n<p>Dos <em>bugs<\/em> de integraci\u00f3n con MapLibre merecen menci\u00f3n porque son del tipo que no aparece en la documentaci\u00f3n. El primero: las fuentes <em>raster<\/em> en MapLibre son inmutables tras su creaci\u00f3n. Si se quiere cambiar la URL (por ejemplo, al cargar una misi\u00f3n diferente), no basta con actualizar la fuente; hay que eliminarla y crearla de nuevo. El segundo: el constructor de <code>TileSet<\/code> recibe como primer argumento la versi\u00f3n del esquema de <em>tiles<\/em> (<code>\"2.1.0\"<\/code>), no el nombre de la capa ni ning\u00fan otro identificador. Pasarle la URL de los <em>tiles<\/em> como primer argumento, que es lo que la l\u00f3gica sugiere, produce un error silencioso.<\/p>\n\n\n\n<p>El tercero fue encontrar la URL correcta: WebODM expone los <em>tiles<\/em> del ortomosaico en <code>\/orthophoto\/tiles\/<\/code>, no en <code>\/tiles\/<\/code>. La diferencia de un segmento de <em>path<\/em> que no aparece documentada de forma clara y que se diagnostic\u00f3 con <code>curl<\/code> directo contra el servidor hasta dar con la ruta que devolv\u00eda 200.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Los hallazgos IA en el mismo mapa<\/h2>\n\n\n\n<p>En paralelo al ortomosaico, la <em>app<\/em> carga tambi\u00e9n los hallazgos del m\u00f3dulo de auditor\u00eda IA directamente en el mapa como una capa GeoJSON. El endpoint <code>\/audit\/project\/{flight_id}\/geojson<\/code> del servidor devuelve una <code>FeatureCollection<\/code> con dos tipos de geometr\u00eda: <code>LineString<\/code> para la trayectoria real del dron durante el vuelo, y <code>Point<\/code> para cada detecci\u00f3n de YOLOv8.<\/p>\n\n\n\n<p>MapLibre filtra las geometr\u00edas por tipo para asignarles capas distintas: la trayectoria se muestra como una l\u00ednea verde sobre el mapa, y las detecciones como puntos rojos. Al tocar uno de esos puntos, <code>queryRenderedFeatures()<\/code> con una tolerancia de 20 p\u00edxeles devuelve los hallazgos cercanos al toque y muestra un <em>popup<\/em> con la clase del objeto detectado y el porcentaje de confianza.<\/p>\n\n\n\n<p>El <em>toggle<\/em> \u00abHallazgos IA\u00bb del selector de capas incluye un contador con el n\u00famero de detecciones del vuelo, calculado a partir del GeoJSON recibido. Las dos capas \u2014ortomosaico y hallazgos\u2014 aparecen en el mismo selector de capas que ya exist\u00eda para OSM, PNOA y Catastro, manteniendo una interfaz unificada para todo lo que puede mostrarse sobre el mapa.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Reanudaci\u00f3n de misi\u00f3n interrumpida<\/h2>\n\n\n\n<p>El problema de las misiones interrumpidas es uno de esos que en el laboratorio parecen un detalle y sobre el terreno resultan cr\u00edticos. El DJI Mini 3 Pro tiene unos 34 minutos de autonom\u00eda con la bater\u00eda est\u00e1ndar que, en la pr\u00e1ctica, se reducen a unos 25 minutos como m\u00e1ximo. Una misi\u00f3n de inspecci\u00f3n de cierta superficie puede requerir m\u00e1s. Y aunque la bater\u00eda aguante, cualquier incidencia \u2014p\u00e9rdida de se\u00f1al, viento inesperado, necesidad de parar\u2014 puede interrumpir la misi\u00f3n a mitad.<\/p>\n\n\n\n<p>La soluci\u00f3n implementada es sencilla en concepto: persistir el \u00edndice del \u00faltimo <em>waypoint<\/em> completado en Room cada vez que el dron alcanza uno, de forma que al reabrir la app y cargar la misi\u00f3n, haya suficiente informaci\u00f3n para continuar desde donde se dej\u00f3.<\/p>\n\n\n\n<p>Cuando una misi\u00f3n tiene progreso guardado, el bot\u00f3n de ejecuci\u00f3n en la pantalla del mapa cambia de \u00abVolar misi\u00f3n\u00bb a \u00abReanudar WP N\u00bb, donde N es el <em>waypoint<\/em> pendiente. Al pulsar reanudar, el dron despega, asciende a la altitud de misi\u00f3n y navega directamente al <em>waypoint<\/em> pendiente, sin recorrer los anteriores. La barra de progreso en la pantalla de ejecuci\u00f3n parte del <em>waypoint<\/em> N, no de cero.<\/p>\n\n\n\n<p>El progreso se limpia autom\u00e1ticamente cuando la misi\u00f3n termina correctamente, de forma que la siguiente vez que se cargue aparezca como misi\u00f3n nueva. Solo persiste si la misi\u00f3n se interrumpi\u00f3 antes de completarse.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Gesti\u00f3n de bater\u00eda: aviso, umbral y RTH autom\u00e1tico<\/h2>\n\n\n\n<p>La gesti\u00f3n de bater\u00eda se implement\u00f3 como una <em>coroutine<\/em> que corre en paralelo a la navegaci\u00f3n principal durante toda la misi\u00f3n, monitorizando el nivel reportado por <code>DroneTelemetry<\/code>. El umbral a partir del cual se activa el retorno autom\u00e1tico es configurable mediante un deslizador en el panel de par\u00e1metros (entre el 10% y el 30%, con pasos de 5%), con 20% como valor por defecto.<\/p>\n\n\n\n<p>El comportamiento tiene dos niveles. Cuando la bater\u00eda cae a umbral+5% aparece un banner amarillo de aviso en la pantalla de ejecuci\u00f3n: el operador sabe que le queda poco margen. Cuando alcanza el umbral exacto, el banner cambia a rojo y el dron inicia RTH autom\u00e1tico. El progreso del <em>waypoint<\/em> actual ya est\u00e1 persistido en Room en ese momento, as\u00ed que al aterrizar, cambiar la bater\u00eda y reconectar, el bot\u00f3n \u00abReanudar WP N\u00bb estar\u00e1 esperando.<\/p>\n\n\n\n<p>El flujo completo de una misi\u00f3n multi-bater\u00eda queda as\u00ed:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Vuelo en curso\n    \u2502\n    \u251c\u2500\u2500 Bater\u00eda cae a umbral+5% \u2192 banner amarillo de aviso\n    \u2502\n    \u251c\u2500\u2500 Bater\u00eda alcanza umbral  \u2192 banner rojo + RTH autom\u00e1tico\n    \u2502                             (progreso guardado en Room)\n    \u2502\n    \u251c\u2500\u2500 Dron aterriza\n    \u2502\n    \u251c\u2500\u2500 Operador: apagar dron \u2192 cambiar bater\u00eda \u2192 encender \u2192 reconectar USB\n    \u2502\n    \u2514\u2500\u2500 Abrir app \u2192 cargar misi\u00f3n \u2192 bot\u00f3n \"Reanudar WP N\"\n              \u2502\n              \u2514\u2500\u2500 Despegar \u2192 volar directo al WP pendiente \u2192 continuar<\/code><\/pre>\n\n\n\n<p>La implementaci\u00f3n tiene algunas protecciones necesarias: ignorar valores de bater\u00eda de 0% (que indican dron desconectado, no bater\u00eda agotada), garantizar que el RTH se dispara una sola vez aunque el nivel oscile alrededor del umbral, y no re-dispararlo si ya est\u00e1 en curso.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Revinculaci\u00f3n de misiones antiguas con proyectos ODM<\/h2>\n\n\n\n<p>Un problema pr\u00e1ctico surgi\u00f3 con las misiones procesadas antes de que existiera la persistencia del <code>odmProjectId<\/code>: esas misiones tienen resultados en WebODM pero la app no sabe a qu\u00e9 proyecto corresponden, as\u00ed que no puede cargar el ortomosaico ni los hallazgos IA.<\/p>\n\n\n\n<p>La soluci\u00f3n fue un bot\u00f3n de revinculaci\u00f3n que aparece en la lista de misiones cuando una misi\u00f3n tiene <code>flight_id<\/code> pero no <code>odmProjectId<\/code>: un icono naranja que al pulsarlo busca en WebODM un proyecto cuyo nombre contenga el prefijo del <code>flight_id<\/code>, extrae el ID de tarea y lo persiste. Un <em>snackbar<\/em> confirma el resultado o muestra el error si no se encuentra ning\u00fan proyecto coincidente.<\/p>\n\n\n\n<p>Durante este desarrollo apareci\u00f3 otro bug de la API de WebODM: el endpoint <code>GET \/projects\/<\/code> devuelve las tareas de cada proyecto como una lista de UUIDs en forma de strings, no como objetos <code>TaskResponse<\/code> completos. El modelo de datos de la app esperaba objetos y la deserializaci\u00f3n fallaba silenciosamente. El cambio fue sencillo \u2014<code>List&lt;TaskResponse&gt;<\/code> por <code>List&lt;String&gt;<\/code> en el modelo\u2014 pero ilustra que la API de WebODM tiene algunas inconsistencias que solo se descubren en la pr\u00e1ctica.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Reorganizaci\u00f3n del selector de capas<\/h2>\n\n\n\n<p>Con la adici\u00f3n de las capas de ortomosaico y hallazgos IA, el selector de capas del mapa empez\u00f3 a solaparse con los FABs de acci\u00f3n (Nueva misi\u00f3n, Cargar, Servidor). La soluci\u00f3n fue doble: mover los chips de overlay (Ortomosaico ODM, Hallazgos IA) dentro del desplegable del selector de capas, junto a las capas base (OSM, PNOA, Catastro); y ocultar los FABs de acci\u00f3n mientras el selector est\u00e1 abierto. Tocar el mapa cierra el selector. El resultado es una interfaz que no se solapa y que agrupa de forma l\u00f3gica todo lo relacionado con la visualizaci\u00f3n del mapa en un \u00fanico control.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">El estado del proyecto al cierre de la Fase 6<\/h2>\n\n\n\n<p>Con esta fase completada, el ciclo de uso de la aplicaci\u00f3n es ya completo de extremo a extremo sin salir de la <em>app<\/em>: planificar la misi\u00f3n sobre el mapa, volarla con el dron de forma aut\u00f3noma, reanudarla si la bater\u00eda se acaba, ver los resultados del procesado ODM como <em>overlay<\/em> cartogr\u00e1fico, y consultar los hallazgos detectados por la IA durante el vuelo con sus coordenadas geogr\u00e1ficas. Todo en la misma pantalla.<\/p>\n\n\n\n<p>En el pr\u00f3ximo art\u00edculo cubrir\u00e9 la Fase 7: la edici\u00f3n de pol\u00edgono post-confirmaci\u00f3n (mover, insertar y borrar v\u00e9rtices sin tener que redibujar el \u00e1rea), la correcci\u00f3n de la estimaci\u00f3n de bater\u00eda \u2014que hasta ahora infraestimaba de forma notable el consumo real\u2014 y la cach\u00e9 <em>offline<\/em> de <em>tiles<\/em> de mapa para trabajar en campo sin cobertura de datos.<\/p>\n","protected":false},"excerpt":{"rendered":"<div class=\"seriesmeta\">Esta entrada es la parte 9 de 9 de la serie <a href=\"https:\/\/bitacora.eniac2000.com\/?series=fotogrametria-asistida-por-ia\" class=\"series-1957\" title=\"Fotogrametr\u00eda asistida por IA\">Fotogrametr\u00eda asistida por IA<\/a><\/div><p>Hasta la Fase 5, la aplicaci\u00f3n era capaz de planificar<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"advanced_seo_description":"","jetpack_seo_html_title":"","jetpack_seo_noindex":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[1845,13],"tags":[537,824,1202,1964],"series":[1957],"class_list":["post-11757","post","type-post","status-publish","format-standard","hentry","category-generado-con-ia","category-informatica","tag-dji-mini-3-pro","tag-ia","tag-open-drone-map","tag-yolo","series-fotogrametria-asistida-por-ia"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11757","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=11757"}],"version-history":[{"count":5,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11757\/revisions"}],"predecessor-version":[{"id":11766,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=\/wp\/v2\/posts\/11757\/revisions\/11766"}],"wp:attachment":[{"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=11757"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=11757"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=11757"},{"taxonomy":"series","embeddable":true,"href":"https:\/\/bitacora.eniac2000.com\/index.php?rest_route=%2Fwp%2Fv2%2Fseries&post=11757"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}