Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/components/Alerts/AlertDetails/AlertImages/AlertImages.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Visibility, VisibilityOff } from '@mui/icons-material';
import {
CropOriginal,
HideImage,
Visibility,
VisibilityOff,
} from '@mui/icons-material';
import DownloadIcon from '@mui/icons-material/Download';
import Divider from '@mui/material/Divider';
import Grid from '@mui/material/Grid';
Expand Down Expand Up @@ -31,6 +36,7 @@ export const AlertImages = ({ sequence }: AlertImagesType) => {
const { t } = useTranslationPrefix('alerts');
const [lastSeenAt, setLastSeenAt] = useState<string | null>(null);
const [displayBbox, setDisplayBbox] = useState(true);
const [displayCrop, setDisplayCrop] = useState(true);
const [orderDetectionsByDesc, setOrderDetectionsByDesc] = useState(true);
const [currentDetection, setCurrentDetection] =
useState<DetectionType | null>(null);
Expand Down Expand Up @@ -85,6 +91,7 @@ export const AlertImages = ({ sequence }: AlertImagesType) => {
useEffect(() => {
// Reset bbox state when the sequence changes
setDisplayBbox(true);
setDisplayCrop(true);
}, [sequence.id]);

const downloadCurrentImage = () => {
Expand Down Expand Up @@ -257,6 +264,15 @@ export const AlertImages = ({ sequence }: AlertImagesType) => {
{displayBbox ? t('buttonHideBBox') : t('buttonDisplayBBox')}
</span>
</ResponsiveButton>
<ResponsiveButton
variant="outlined"
onClick={() => setDisplayCrop((oldValue) => !oldValue)}
startIcon={displayCrop ? <HideImage /> : <CropOriginal />}
>
<span className="Mui-label">
{displayCrop ? t('buttonHideCrop') : t('buttonDisplayCrop')}
</span>
</ResponsiveButton>
<SplitButton
label={t('buttonImageDownload')}
options={downloadOptions}
Expand Down Expand Up @@ -285,6 +301,7 @@ export const AlertImages = ({ sequence }: AlertImagesType) => {
sequenceId={sequence.id}
detections={detectionsList}
displayBbox={displayBbox}
displayCrop={displayCrop}
onSelectedDetectionChange={setCurrentDetection}
firstConfidentDetectionIndex={getFirstConfidentDetectionIndex(
detectionsList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ interface AlertImagesPlayerType {
sequenceId: number;
detections: DetectionType[]; // Sorted
displayBbox: boolean;
displayCrop: boolean;
onSelectedDetectionChange: (detection: DetectionType | null) => void;
firstConfidentDetectionIndex: number;
orderDetectionsByDesc: boolean;
Expand All @@ -35,6 +36,7 @@ export const AlertImagesPlayer = ({
sequenceId,
detections,
displayBbox,
displayCrop,
onSelectedDetectionChange,
firstConfidentDetectionIndex,
orderDetectionsByDesc,
Expand Down Expand Up @@ -126,6 +128,7 @@ export const AlertImagesPlayer = ({
<Stack direction="column" spacing={1}>
<DetectionImageWithBoundingBox
displayBbox={displayBbox}
displayCrop={displayCrop}
sequenceId={sequenceId}
selectedDetection={selectedDetection}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useTheme } from '@mui/material';
import { useEffect, useRef, useState } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import {
MiniMap,
type ReactZoomPanPinchContentRef,
Expand All @@ -11,19 +11,30 @@ import type { DetectionType } from '@//services/alerts';

const MINIMUM_ZOOM_AMOUNT_TO_DISPLAY_MINIMAP = 1.4;

const parseBboxCoords = (
bbox: string
): { x1: number; y1: number; x2: number; y2: number } | null => {
const match = /\(([^)]+)\)/.exec(bbox);
if (!match) {
return null;
}
const [x1, y1, x2, y2] = match[1].split(',').map(parseFloat);
return { x1, y1, x2, y2 };
};

const parseDetectionBox = (
detection: DetectionType | null
): BoundingBox | null => {
if (detection === null) {
return null;
}

const match = /\(([^)]+)\)/.exec(detection.bbox);
if (!match) {
const coords = parseBboxCoords(detection.bbox);
if (!coords) {
return null;
}

const [x1, y1, x2, y2] = match[1].split(',').map(parseFloat);
const { x1, y1, x2, y2 } = coords;
return {
left: `${100 * x1}%`,
top: `${100 * y1}%`,
Expand All @@ -41,12 +52,14 @@ interface BoundingBox {

interface DetectionImageWithBoundingBoxProps {
displayBbox: boolean;
displayCrop: boolean;
selectedDetection: DetectionType;
sequenceId: number;
}

export const DetectionImageWithBoundingBox = ({
displayBbox,
displayCrop,
selectedDetection,
sequenceId,
}: DetectionImageWithBoundingBoxProps) => {
Expand All @@ -55,6 +68,16 @@ export const DetectionImageWithBoundingBox = ({
const [currentBox, setCurrentBox] = useState<BoundingBox | null>(null);
const shouldResetTransform = useRef(false);

// Place the crop preview opposite the detection so it never sits on top of it
const cropOnLeft = useMemo(() => {
const coords = parseBboxCoords(selectedDetection.bbox);
if (!coords) {
return true;
}
const centerX = (coords.x1 + coords.x2) / 2;
return centerX > 0.5;
}, [selectedDetection.bbox]);

// Reset image position & zoom whenever the user switches alert
// This reset will happen once the new image has loaded to avoid glitchy looking behaviour
useEffect(() => {
Expand Down Expand Up @@ -126,6 +149,24 @@ export const DetectionImageWithBoundingBox = ({
</div>
)}

{displayCrop && selectedDetection.crop_url && (
<img
src={selectedDetection.crop_url}
alt=""
style={{
position: 'absolute',
bottom: 20,
...(cropOnLeft ? { left: 20 } : { right: 20 }),
width: 150,
zIndex: 2,
border: `2px solid ${theme.palette.secondary.dark}`,
borderRadius: 4,
boxShadow: '0 2px 6px rgba(0, 0, 0, 0.4)',
backgroundColor: theme.palette.background.paper,
}}
/>
)}

<TransformComponent>
<img
ref={imgRef}
Expand Down
2 changes: 2 additions & 0 deletions src/locales/de/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"buttonPause": "Pause",
"buttonDisplayBBox": "Erkennung anzeigen",
"buttonHideBBox": "Erkennung verbergen",
"buttonDisplayCrop": "Nahaufnahme anzeigen",
"buttonHideCrop": "Nahaufnahme verbergen",
"errorFetchImagesMessage": "Auf unserer Seite ist etwas schief gelaufen. Bitte versuchen Sie es später erneut.",
"titleDetails": "Informationen zum Alarm",
"titleButtonBackAlt": "Zurück zur Alarmliste",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"buttonPause": "Pause",
"buttonDisplayBBox": "Display the detection",
"buttonHideBBox": "Hide the detection",
"buttonDisplayCrop": "Show close-up",
"buttonHideCrop": "Hide close-up",
"errorFetchImagesMessage": "Something went wrong on our side. Please try again later.",
"titleDetails": "Information about the alert",
"titleButtonBackAlt": "Back to alerts list",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/es/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
"buttonPause": "Pause",
"buttonDisplayBBox": "Mostrar la detección",
"buttonHideBBox": "Ocultar la detección",
"buttonDisplayCrop": "Mostrar primer plano",
"buttonHideCrop": "Ocultar primer plano",
"errorFetchImagesMessage": "Se ha producido un error. Vuelva a intentarlo más tarde.",
"titleDetails": "Detalles de la alerta",
"titleButtonBackAlt": "Volver a la lista",
Expand Down
2 changes: 2 additions & 0 deletions src/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
"buttonPause": "Pause",
"buttonDisplayBBox": "Afficher la détection",
"buttonHideBBox": "Masquer la détection",
"buttonDisplayCrop": "Afficher le gros plan",
"buttonHideCrop": "Masquer le gros plan",
"errorFetchImagesMessage": "Une erreur inattendue est survenue. Veuillez réessayer plus tard.",
"titleDetails": "Détail de l'alerte",
"subtitleDate": "Date & Heure",
Expand Down
3 changes: 2 additions & 1 deletion src/services/alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const apiDetectionResponseSchema = z.object({
others_bboxes: z.nullable(z.string()),
created_at: z.iso.datetime({ local: true }),
url: z.string(),
crop_url: z.string().nullish(),
});

export type SequenceTypeApi = z.infer<typeof apiSequenceResponseSchema>;
Expand Down Expand Up @@ -128,7 +129,7 @@ export const getDetectionsBySequence = async (
): Promise<DetectionType[]> => {
return apiInstance
.get(`/api/v1/sequences/${sequenceId.toString()}/detections`, {
params: { desc: isDesc },
params: { with_crop: true, desc: isDesc },
})
.then((response: AxiosResponse) => {
try {
Expand Down
Loading