2nzi's picture
first commit
2964111 verified
<template>
<div class="calibration">
<!-- Bouton retour home -->
<button @click="goBack" class="btn-home" title="Retour à l'accueil">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9,22 9,12 15,12 15,22"/>
</svg>
</button>
<div class="main-content">
<div class="content-area">
<div class="video-display">
<div class="calibration-container">
<CalibrationArea
ref="calibrationArea"
:thumbnail="thumbnail"
:calibrationPoints="calibrationPoints"
:calibrationLines="calibrationLines"
:selectedFieldPoint="selectedFieldPoint"
:selectedFieldLine="selectedFieldLine"
@update:thumbnail="updateThumbnail"
@update:calibrationPoints="updateCalibrationPoints"
@update:calibrationLines="updateCalibrationLines"
@update:selectedFieldPoint="updateSelectedFieldPoint"
@update:selectedFieldLine="updateSelectedFieldLine"
@clear-calibration="clearCalibration"
@process-calibration="processCalibration"
/>
</div>
<div class="field-container">
<FootballField
ref="footballField"
@point-selected="handleFieldPointSelected"
@line-selected="handleFieldLineSelected"
:positionedPoints="calibrationPoints"
:positionedLines="calibrationLines"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import CalibrationArea from '@/components/CalibrationArea.vue'
import FootballField from '@/components/FootballField.vue'
import { useUploadStore } from '@/stores/upload'
import { useCalibrationStore } from '@/stores/calibration'
import api from '@/services/api'
export default {
name: 'ManualView',
components: {
CalibrationArea,
FootballField
},
setup() {
const uploadStore = useUploadStore()
const calibrationStore = useCalibrationStore()
return {
uploadStore,
calibrationStore
}
},
data() {
return {
thumbnail: null,
calibrationPoints: {},
selectedFieldPoint: null,
calibrationLines: {},
selectedFieldLine: null
}
},
computed: {
canProcess() {
return Object.keys(this.calibrationLines).length > 0 || Object.keys(this.calibrationPoints).length > 0
}
},
async created() {
// Vérifier qu'un fichier est sélectionné
if (!this.uploadStore.selectedFile) {
this.$router.push('/')
return
}
// Appliquer les styles fullscreen
this.applyFullscreenStyles()
// Charger l'image/vidéo
await this.loadThumbnail()
},
beforeUnmount() {
// Restaurer les styles originaux
this.removeFullscreenStyles()
},
methods: {
goBack() {
this.removeFullscreenStyles()
this.$router.push('/')
},
applyFullscreenStyles() {
const app = document.getElementById('app')
if (app) {
app.style.maxWidth = 'none'
app.style.margin = '0'
app.style.padding = '0'
}
},
removeFullscreenStyles() {
const app = document.getElementById('app')
if (app) {
app.style.maxWidth = '1280px'
app.style.margin = '0 auto'
app.style.padding = '1rem'
}
},
async loadThumbnail() {
try {
this.thumbnail = null
if (this.uploadStore.isImage) {
// Pour les images, utiliser directement la preview
this.thumbnail = this.uploadStore.filePreview
} else if (this.uploadStore.isStaticVideo && this.uploadStore.extractedFrame) {
// Pour les vidéos statiques, utiliser la frame déjà extraite
console.log('Using extracted frame from static video:', this.uploadStore.selectedFile.name)
this.thumbnail = URL.createObjectURL(this.uploadStore.extractedFrame)
} else if (this.uploadStore.isVideo) {
// Pour les autres vidéos, extraire la première frame
console.log('Extracting first frame from video:', this.uploadStore.selectedFile.name)
// Créer une URL pour le fichier vidéo
const videoUrl = URL.createObjectURL(this.uploadStore.selectedFile)
// Extraire la première frame avec un canvas
const video = document.createElement('video')
video.src = videoUrl
video.muted = true // Important pour éviter les problèmes d'autoplay
await new Promise((resolve, reject) => {
video.addEventListener('loadedmetadata', () => {
video.currentTime = 0 // Aller à la première frame
})
video.addEventListener('seeked', () => {
try {
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
canvas.height = video.videoHeight
const ctx = canvas.getContext('2d')
ctx.drawImage(video, 0, 0)
this.thumbnail = canvas.toDataURL('image/jpeg', 0.9)
URL.revokeObjectURL(videoUrl)
resolve()
} catch (err) {
URL.revokeObjectURL(videoUrl)
reject(err)
}
})
video.addEventListener('error', (err) => {
URL.revokeObjectURL(videoUrl)
reject(err)
})
})
}
} catch (error) {
console.error('Erreur lors du chargement du thumbnail:', error)
this.thumbnail = null
}
},
handleFieldPointSelected(pointData) {
this.selectedFieldLine = null
this.selectedFieldPoint = pointData
},
handleFieldLineSelected(lineData) {
this.selectedFieldPoint = null
this.selectedFieldLine = lineData
},
updateThumbnail(newThumbnail) {
this.thumbnail = newThumbnail
},
updateCalibrationPoints(newPoints) {
this.calibrationPoints = { ...newPoints }
},
updateCalibrationLines(newLines) {
this.calibrationLines = { ...newLines }
},
updateSelectedFieldPoint(newPoint) {
this.selectedFieldPoint = newPoint
},
updateSelectedFieldLine(newLine) {
this.selectedFieldLine = newLine
if (this.$refs.footballField) {
this.$refs.footballField.selectedLine = newLine ? newLine.id : null
}
},
clearCalibration() {
this.calibrationPoints = {}
this.calibrationLines = {}
this.selectedFieldPoint = null
this.selectedFieldLine = null
},
async processCalibration() {
if (!this.uploadStore.selectedFile || Object.keys(this.calibrationLines).length === 0) {
alert('Veuillez créer au moins une ligne de calibration')
return
}
try {
this.calibrationStore.setProcessing(true, 'Traitement de la calibration manuelle...')
if (!this.$refs.calibrationArea) {
console.error('CalibrationArea component is not mounted.')
return
}
const imageContainer = document.querySelector('.video-frame')
const imageSize = this.$refs.calibrationArea.imageSize
if (!imageSize) {
console.error('Image size is not available.')
return
}
const containerWidth = imageContainer.clientWidth
const containerHeight = imageContainer.clientHeight
// Préparer les données des lignes pour l'API /calibrate
const linesData = {}
// Traitement des lignes - conversion des coordonnées container vers image
for (const [lineName, line] of Object.entries(this.calibrationLines)) {
linesData[lineName] = line.points.map(point => {
return {
x: point.x / containerWidth * imageSize.width,
y: point.y / containerHeight * imageSize.height
}
})
}
console.log('🔥 Données de lignes pour calibration:', linesData)
// Appeler l'API /calibrate avec l'image et les lignes
const result = await api.calibrateCamera(this.uploadStore.selectedFile, linesData)
console.log('🔥 Réponse API calibration:', result)
if (result.status === 'success') {
this.calibrationStore.setResults(result)
this.$router.push('/')
} else if (result.status === 'failed') {
throw new Error(result.message || "Échec du traitement de la calibration")
} else {
throw new Error(result.error || 'Erreur de traitement')
}
} catch (error) {
console.error('❌ Erreur lors du traitement manuel:', error)
this.calibrationStore.setError(error.message)
this.$router.push('/')
}
}
}
}
</script>
<style scoped>
.calibration {
height: 100vh;
display: flex;
flex-direction: column;
color: white;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.btn-home {
position: fixed;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.1);
color: #888;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 10px;
cursor: pointer;
transition: all 0.3s ease;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
}
.btn-home:hover {
background: rgba(255, 255, 255, 0.15);
color: var(--color-primary);
border-color: var(--color-primary);
transform: scale(1.05);
}
.btn-home svg {
transition: all 0.3s ease;
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
.content-area {
flex: 1;
display: flex;
flex-direction: column;
padding: 0;
}
.video-display {
flex: 1;
display: flex;
gap: 15px;
padding: 15px;
overflow: hidden;
height: 100%;
}
.calibration-container {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
height: 100%;
}
.field-container {
flex: 1;
min-width: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.field-container :deep(svg) {
width: 100%;
height: 100%;
object-fit: contain;
margin-top: -10px;
}
.calibration-container :deep(.video-frame-container) {
height: 100%;
}
.calibration-container :deep(.video-frame) {
height: calc(100% - 80px);
}
.actions {
display: flex;
justify-content: center;
background-color: #2a2a2a;
border-top: 1px solid #333;
}
.btn-process {
background: var(--color-primary);
color: var(--color-secondary);
border: none;
padding: 1rem 2rem;
border-radius: 8px;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
}
.btn-process:hover:not(:disabled) {
background: var(--color-primary-soft);
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(217, 255, 4, 0.4);
}
.btn-process:disabled {
background: #555;
color: #888;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* Responsive */
@media (max-width: 768px) {
.video-display {
flex-direction: column;
}
.header-actions {
padding: 1rem;
}
.header-actions h1 {
font-size: 1.2rem;
}
.file-info {
display: none;
}
}
</style>