Skip to content

Commit afe8df6

Browse files
committed
feat: Add controllers, routes, and services for camera heatmaps in the backend and frontend
1 parent a315860 commit afe8df6

8 files changed

Lines changed: 285 additions & 121 deletions

File tree

data/postgresDB/init.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ CREATE TABLE IF NOT EXISTS users (
6767
-- Tabla cameras
6868
CREATE TABLE IF NOT EXISTS cameras (
6969
id SERIAL PRIMARY KEY,
70+
camera_id VARCHAR,
7071
user_id INT NOT NULL,
7172
name VARCHAR NOT NULL,
7273
source_type camera_source_enum NOT NULL,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import axios from 'axios';
2+
import { Request, Response } from 'express';
3+
import globalMessages from '../utils/messages/global.messages';
4+
import redis from '../config/redis.config';
5+
import globalConfig from '../config/global.config';
6+
7+
const getCameraIdFromQuery = (req: Request): string => {
8+
const cameraId = req.query.camera_id as string;
9+
if (!cameraId) throw new Error("Parámetro 'camera_id' requerido en la query.");
10+
return cameraId;
11+
};
12+
13+
const getTokenFromHeader = (req: Request): string => {
14+
const token = req.headers['x-access-token'] as string;
15+
if (!token) throw new Error("Token de acceso no proporcionado en 'x-access-token'.");
16+
return token;
17+
};
18+
19+
export const getSegmentData = async (req: Request, res: Response) => {
20+
try {
21+
const cameraId = getCameraIdFromQuery(req);
22+
const token = getTokenFromHeader(req);
23+
const cacheKey = `coordinate_segment_data_${cameraId}`;
24+
const cached = await redis.get(cacheKey);
25+
if (cached) return res.status(200).json(JSON.parse(cached));
26+
27+
const url = `${globalConfig.pythonURL}/coordinate/segment-data?camera_id=${cameraId}`;
28+
const response = await axios.get(url, {
29+
headers: { Authorization: `Bearer ${token}` }
30+
});
31+
32+
console.log("Segment Data Response:", JSON.stringify(response.data, null, 2));
33+
34+
await redis.set(cacheKey, JSON.stringify(response.data), "EX", 3600);
35+
return res.status(200).json(response.data);
36+
} catch (error) {
37+
console.error("Error getting segment data:", error);
38+
return res.status(500).json({ message: globalMessages[500].INTERNAL_SERVER_ERROR });
39+
}
40+
};
41+
42+
export const getBirdStatistics = async (req: Request, res: Response) => {
43+
try {
44+
const cameraId = getCameraIdFromQuery(req);
45+
const token = getTokenFromHeader(req);
46+
const cacheKey = `coordinate_bird_statistics_${cameraId}`;
47+
const cached = await redis.get(cacheKey);
48+
if (cached) return res.status(200).json(JSON.parse(cached));
49+
50+
const url = `${globalConfig.pythonURL}/coordinate/bird-statistics/by-camera?camera_id=${cameraId}`;
51+
const response = await axios.get(url, {
52+
headers: { Authorization: `Bearer ${token}` }
53+
});
54+
55+
console.log("Bird Statistics Response:", JSON.stringify(response.data, null, 2));
56+
57+
await redis.set(cacheKey, JSON.stringify(response.data), "EX", 3600);
58+
return res.status(200).json(response.data);
59+
} catch (error) {
60+
console.error("Error getting bird statistics:", error);
61+
return res.status(500).json({ message: globalMessages[500].INTERNAL_SERVER_ERROR });
62+
}
63+
};
64+
65+
export const getHeatmapData = async (req: Request, res: Response) => {
66+
try {
67+
const cameraId = getCameraIdFromQuery(req);
68+
const token = getTokenFromHeader(req);
69+
const cacheKey = `coordinate_heatmap_data_${cameraId}`;
70+
const cached = await redis.get(cacheKey);
71+
if (cached) return res.status(200).json(JSON.parse(cached));
72+
73+
const url = `${globalConfig.pythonURL}/coordinate/heatmap-data?camera_id=${cameraId}`;
74+
const response = await axios.get(url, {
75+
headers: { Authorization: `Bearer ${token}` }
76+
});
77+
78+
console.log("Heatmap Data Response:", JSON.stringify(response.data, null, 2));
79+
80+
await redis.set(cacheKey, JSON.stringify(response.data), "EX", 3600);
81+
return res.status(200).json(response.data);
82+
} catch (error) {
83+
console.error("Error getting heatmap data:", error);
84+
return res.status(500).json({ message: globalMessages[500].INTERNAL_SERVER_ERROR });
85+
}
86+
};

ia4birds-all/ai4birds-web/ai4birds-web-app-backend/routes/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import uploadRoutes from "./routes/upload.routes";
33
import authRoutes from "./routes/auth.routes";
44
import userRoutes from "./routes/user.routes";
55
import cameraRoutes from "./routes/camera.routes";
6+
import dataRouter from "./routes/data.routes"
67

78
export default (): Router => {
89
const mainRouter = Router();
910
mainRouter.use("/data", uploadRoutes());
1011
mainRouter.use("/auth", authRoutes());
1112
mainRouter.use("/user", userRoutes());
1213
mainRouter.use("/camera", cameraRoutes());
14+
mainRouter.use("/data-camera", dataRouter());
1315

1416
return mainRouter;
1517
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Request, Response, NextFunction, Express, Router } from 'express'
2+
import * as coordinateController from "../../controllers/coordinate.controller";
3+
import authMiddleware from "../../middleware/auth.middleware";
4+
5+
export default () => {
6+
7+
const dataRouter: Router = Router();
8+
9+
dataRouter.get(
10+
"/coordinate/segment-data",
11+
authMiddleware.verifyToken,
12+
coordinateController.getSegmentData
13+
)
14+
15+
dataRouter.get(
16+
"/coordinate/bird-statistics",
17+
authMiddleware.verifyToken,
18+
coordinateController.getBirdStatistics
19+
)
20+
21+
dataRouter.get(
22+
"/coordinate/heatmap-data",
23+
authMiddleware.verifyToken,
24+
coordinateController.getHeatmapData
25+
)
26+
27+
return dataRouter;
28+
};
29+
30+

ia4birds-all/ai4birds-web/ai4birds-web-app-backend/scripts/initCamera.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const insertDefaultCamera = async () => {
66
const name = "camara_air";
77
const source_url = "rtsp://root:Airinstitute00@10.189.0.88:10554/axis-media/media.amp";
88
const location = "Air Institute";
9+
const camera_id = "AXIS_Q6225-LE_PTZ";
910

1011
try {
1112
const existing = await Camera.findOne({ where: { name } });
@@ -45,7 +46,8 @@ const insertDefaultCamera = async () => {
4546
storage_info: null,
4647
additional_data: null,
4748
is_public: true,
48-
user_id: 1
49+
user_id: 1,
50+
camera_id
4951
});
5052

5153
console.log("Default camera registered in MediaMTX and database.");

ia4birds-all/ai4birds-web/ai4birds-web-app-frontend/src/assets/components/card/CameraPanelCardHeatMap.tsx

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@nextu
66
import Hls from "hls.js";
77
import { RxTrash, RxMagnifyingGlass, RxZoomIn, RxZoomOut, RxReset } from "react-icons/rx";
88
import heatmapImage from "../../images/heatmap.png";
9+
import BirdCoordinateService from "../services/BirdCoordinateService";
910

1011
export const CustomCardCameraHeatMap = ({ cameraPanelData, onDelete }) => {
1112
const [heatmapImageUrl, setHeatmapImageUrl] = useState<string | null>(null);
@@ -17,6 +18,7 @@ export const CustomCardCameraHeatMap = ({ cameraPanelData, onDelete }) => {
1718
const [isDragging, setIsDragging] = useState(false);
1819
const [dragStart, setDragStart] = useState({ x: 0, y: 0 });
1920
const imageRef = useRef<HTMLImageElement>(null);
21+
const [heatmapMetadata, setHeatmapMetadata] = useState(null);
2022

2123
const handleDeleteClick = () => {
2224
if (onDelete && cameraPanelData.id) {
@@ -26,41 +28,31 @@ export const CustomCardCameraHeatMap = ({ cameraPanelData, onDelete }) => {
2628

2729
// Aqui obtengo url de back para el heatmap
2830
const fetchHeatmap = async () => {
29-
if (!cameraPanelData?.id) return;
31+
const cameraId = cameraPanelData?.camera_id;
32+
if (!cameraId) return;
3033

3134
setIsLoading(true);
3235
setError(null);
3336

3437
try {
3538
const response = await fetch(`/api/cameras/${cameraPanelData.id}/heatmap`);
36-
37-
if (!response.ok) {
38-
// BORRAR ESTE IF CUANDO EXISTA EL ENDPOINT, ES PARA QUE NO APAREZCA UN ERROR YA QUE ACTUALMETNE NO ESTÁ HECHO EN BACK
39-
if (response.status === 404) {
40-
console.log("Endpoint de heatmap no disponible, usando imagen local");
41-
setHeatmapImageUrl(heatmapImage);
42-
return;
43-
}
44-
throw new Error(`Error ${response.status}: ${response.statusText}`);
45-
}
46-
47-
// BORRAR ESTE CONST E IF CUANDO EXISTA EL ENDPOINT, ES PARA QUE NO APAREZCA UN ERROR YA QUE ACTUALMETNE NO ESTÁ HECHO EN BACK
48-
const contentType = response.headers.get('content-type');
49-
if (!contentType || !contentType.includes('application/json')) {
50-
console.log("Respuesta no es JSON, usando imagen local");
51-
setHeatmapImageUrl(heatmapImage);
39+
const heatmapData = await BirdCoordinateService.getHeatmapData(cameraId)
40+
41+
if (!heatmapData?.image_url) {
42+
setError("No se encontró la imagen del mapa de calor");
43+
setHeatmapImageUrl(null);
5244
return;
5345
}
46+
47+
setHeatmapMetadata(heatmapData);
48+
setHeatmapImageUrl(heatmapData.image_url);
5449

55-
const data = await response.json();
56-
setHeatmapImageUrl(data.heatmapUrl);
5750

5851
} catch (error) {
5952
console.error("Error fetching heatmap:", error);
53+
setError("No se pudo cargar el mapa de calor");
54+
setHeatmapImageUrl(null);
6055

61-
// DESCOMENTAR ESTA LÍNEA CUANDO EXISTA EL ENDPOINT
62-
// setError(error instanceof Error ? error.message : "Error desconocido");
63-
setHeatmapImageUrl(heatmapImage);
6456
} finally {
6557
setIsLoading(false);
6658
}
@@ -75,7 +67,7 @@ export const CustomCardCameraHeatMap = ({ cameraPanelData, onDelete }) => {
7567
URL.revokeObjectURL(heatmapImageUrl);
7668
}
7769
};
78-
}, [cameraPanelData?.id]);
70+
}, [cameraPanelData?.camera_id]);
7971

8072
const handleRefresh = () => {
8173
fetchHeatmap();
@@ -184,11 +176,12 @@ export const CustomCardCameraHeatMap = ({ cameraPanelData, onDelete }) => {
184176
</Button>
185177
</div>
186178
</div>
187-
<div className="mt-4 text-sm text-gray-600 text-center max-w-md">
188-
<p><strong>Descripción:</strong> Mapa de calor que muestra las zonas de mayor actividad de aves detectadas por la cámara.</p>
189-
<p><strong>Última actualización:</strong> {new Date().toLocaleString()}</p>
190-
<p className="text-xs text-gray-400 mt-1">Haz clic en la imagen para ampliar</p>
191-
</div>
179+
{heatmapMetadata && (
180+
<div className="mt-4 text-sm text-gray-600 text-center max-w-md">
181+
<p><strong>Descripción:</strong> {heatmapMetadata.heatmap_for}</p>
182+
<p><strong>Última generación:</strong> {new Date(heatmapMetadata.generated_at).toLocaleString()}</p>
183+
</div>
184+
)}
192185
</div>
193186
)}
194187

0 commit comments

Comments
 (0)