Spaces:
Running
Running
Plotly chart
Browse files- package-lock.json +66 -0
- package.json +3 -1
- src/components/PlotlyChart.vue +675 -0
- src/services/api.js +2 -2
- src/views/HomeView.vue +84 -13
package-lock.json
CHANGED
@@ -8,7 +8,9 @@
|
|
8 |
"name": "soccer-field-app",
|
9 |
"version": "0.0.0",
|
10 |
"dependencies": {
|
|
|
11 |
"pinia": "^3.0.1",
|
|
|
12 |
"vue": "^3.5.13",
|
13 |
"vue-router": "^4.5.0"
|
14 |
},
|
@@ -2066,6 +2068,15 @@
|
|
2066 |
"dev": true,
|
2067 |
"license": "MIT"
|
2068 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2069 |
"node_modules/concat-map": {
|
2070 |
"version": "0.0.1",
|
2071 |
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
@@ -2129,6 +2140,31 @@
|
|
2129 |
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
2130 |
"license": "MIT"
|
2131 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2132 |
"node_modules/debug": {
|
2133 |
"version": "4.4.1",
|
2134 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
@@ -2773,6 +2809,18 @@
|
|
2773 |
"node": ">=18.18.0"
|
2774 |
}
|
2775 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2776 |
"node_modules/ignore": {
|
2777 |
"version": "5.3.2",
|
2778 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
@@ -3382,6 +3430,12 @@
|
|
3382 |
}
|
3383 |
}
|
3384 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
3385 |
"node_modules/postcss": {
|
3386 |
"version": "8.5.6",
|
3387 |
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
@@ -3565,6 +3619,18 @@
|
|
3565 |
"url": "https://github.com/sponsors/sindresorhus"
|
3566 |
}
|
3567 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3568 |
"node_modules/semver": {
|
3569 |
"version": "7.7.2",
|
3570 |
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
|
|
8 |
"name": "soccer-field-app",
|
9 |
"version": "0.0.0",
|
10 |
"dependencies": {
|
11 |
+
"d3-dsv": "^3.0.1",
|
12 |
"pinia": "^3.0.1",
|
13 |
+
"plotly.js-dist": "^2.29.1",
|
14 |
"vue": "^3.5.13",
|
15 |
"vue-router": "^4.5.0"
|
16 |
},
|
|
|
2068 |
"dev": true,
|
2069 |
"license": "MIT"
|
2070 |
},
|
2071 |
+
"node_modules/commander": {
|
2072 |
+
"version": "7.2.0",
|
2073 |
+
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
2074 |
+
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
2075 |
+
"license": "MIT",
|
2076 |
+
"engines": {
|
2077 |
+
"node": ">= 10"
|
2078 |
+
}
|
2079 |
+
},
|
2080 |
"node_modules/concat-map": {
|
2081 |
"version": "0.0.1",
|
2082 |
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
|
|
2140 |
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
2141 |
"license": "MIT"
|
2142 |
},
|
2143 |
+
"node_modules/d3-dsv": {
|
2144 |
+
"version": "3.0.1",
|
2145 |
+
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
2146 |
+
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
|
2147 |
+
"license": "ISC",
|
2148 |
+
"dependencies": {
|
2149 |
+
"commander": "7",
|
2150 |
+
"iconv-lite": "0.6",
|
2151 |
+
"rw": "1"
|
2152 |
+
},
|
2153 |
+
"bin": {
|
2154 |
+
"csv2json": "bin/dsv2json.js",
|
2155 |
+
"csv2tsv": "bin/dsv2dsv.js",
|
2156 |
+
"dsv2dsv": "bin/dsv2dsv.js",
|
2157 |
+
"dsv2json": "bin/dsv2json.js",
|
2158 |
+
"json2csv": "bin/json2dsv.js",
|
2159 |
+
"json2dsv": "bin/json2dsv.js",
|
2160 |
+
"json2tsv": "bin/json2dsv.js",
|
2161 |
+
"tsv2csv": "bin/dsv2dsv.js",
|
2162 |
+
"tsv2json": "bin/dsv2json.js"
|
2163 |
+
},
|
2164 |
+
"engines": {
|
2165 |
+
"node": ">=12"
|
2166 |
+
}
|
2167 |
+
},
|
2168 |
"node_modules/debug": {
|
2169 |
"version": "4.4.1",
|
2170 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
|
|
2809 |
"node": ">=18.18.0"
|
2810 |
}
|
2811 |
},
|
2812 |
+
"node_modules/iconv-lite": {
|
2813 |
+
"version": "0.6.3",
|
2814 |
+
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
2815 |
+
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
2816 |
+
"license": "MIT",
|
2817 |
+
"dependencies": {
|
2818 |
+
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
2819 |
+
},
|
2820 |
+
"engines": {
|
2821 |
+
"node": ">=0.10.0"
|
2822 |
+
}
|
2823 |
+
},
|
2824 |
"node_modules/ignore": {
|
2825 |
"version": "5.3.2",
|
2826 |
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
|
|
3430 |
}
|
3431 |
}
|
3432 |
},
|
3433 |
+
"node_modules/plotly.js-dist": {
|
3434 |
+
"version": "2.35.3",
|
3435 |
+
"resolved": "https://registry.npmjs.org/plotly.js-dist/-/plotly.js-dist-2.35.3.tgz",
|
3436 |
+
"integrity": "sha512-dqB9+FUyBFZN04xWnZoYwaeeF4Jj9T/m0CHYmoozmPC3R4Dy0TRJsHgbRVLPxgYQqodzniVUj17+2wmJuGaZAg==",
|
3437 |
+
"license": "MIT"
|
3438 |
+
},
|
3439 |
"node_modules/postcss": {
|
3440 |
"version": "8.5.6",
|
3441 |
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
|
|
3619 |
"url": "https://github.com/sponsors/sindresorhus"
|
3620 |
}
|
3621 |
},
|
3622 |
+
"node_modules/rw": {
|
3623 |
+
"version": "1.3.3",
|
3624 |
+
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
3625 |
+
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
3626 |
+
"license": "BSD-3-Clause"
|
3627 |
+
},
|
3628 |
+
"node_modules/safer-buffer": {
|
3629 |
+
"version": "2.1.2",
|
3630 |
+
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
3631 |
+
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
3632 |
+
"license": "MIT"
|
3633 |
+
},
|
3634 |
"node_modules/semver": {
|
3635 |
"version": "7.7.2",
|
3636 |
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
package.json
CHANGED
@@ -13,7 +13,9 @@
|
|
13 |
"dependencies": {
|
14 |
"pinia": "^3.0.1",
|
15 |
"vue": "^3.5.13",
|
16 |
-
"vue-router": "^4.5.0"
|
|
|
|
|
17 |
},
|
18 |
"devDependencies": {
|
19 |
"@eslint/js": "^9.22.0",
|
|
|
13 |
"dependencies": {
|
14 |
"pinia": "^3.0.1",
|
15 |
"vue": "^3.5.13",
|
16 |
+
"vue-router": "^4.5.0",
|
17 |
+
"plotly.js-dist": "^2.29.1",
|
18 |
+
"d3-dsv": "^3.0.1"
|
19 |
},
|
20 |
"devDependencies": {
|
21 |
"@eslint/js": "^9.22.0",
|
src/components/PlotlyChart.vue
ADDED
@@ -0,0 +1,675 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<template>
|
2 |
+
<div class="plotly-chart">
|
3 |
+
<div class="chart-header">
|
4 |
+
<h3>{{ title }}</h3>
|
5 |
+
<button v-if="loading" class="refresh-btn" disabled>
|
6 |
+
Chargement...
|
7 |
+
</button>
|
8 |
+
|
9 |
+
</div>
|
10 |
+
|
11 |
+
<div
|
12 |
+
ref="plotlyDiv"
|
13 |
+
:id="chartId"
|
14 |
+
class="chart-container"
|
15 |
+
:class="{ loading: loading }"
|
16 |
+
>
|
17 |
+
<div v-if="loading" class="loading-spinner">
|
18 |
+
<div class="spinner"></div>
|
19 |
+
<p>Chargement des données...</p>
|
20 |
+
</div>
|
21 |
+
|
22 |
+
</div>
|
23 |
+
</div>
|
24 |
+
</template>
|
25 |
+
|
26 |
+
<script>
|
27 |
+
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
|
28 |
+
import Plotly from 'plotly.js-dist'
|
29 |
+
import { csvParse } from 'd3-dsv'
|
30 |
+
|
31 |
+
export default {
|
32 |
+
name: 'PlotlyChart',
|
33 |
+
props: {
|
34 |
+
title: {
|
35 |
+
type: String,
|
36 |
+
default: ''
|
37 |
+
},
|
38 |
+
csvUrl: {
|
39 |
+
type: String,
|
40 |
+
default: ''
|
41 |
+
},
|
42 |
+
dataType: {
|
43 |
+
type: String,
|
44 |
+
default: 'csv', // 'csv' ou 'football-field'
|
45 |
+
validator: (value) => ['csv', 'football-field'].includes(value)
|
46 |
+
},
|
47 |
+
chartId: {
|
48 |
+
type: String,
|
49 |
+
default: () => ``
|
50 |
+
},
|
51 |
+
height: {
|
52 |
+
type: [String, Number],
|
53 |
+
default: 500
|
54 |
+
},
|
55 |
+
customLayout: {
|
56 |
+
type: Object,
|
57 |
+
default: () => ({})
|
58 |
+
},
|
59 |
+
keypointsData: {
|
60 |
+
type: Array,
|
61 |
+
default: () => []
|
62 |
+
},
|
63 |
+
cameraParams: {
|
64 |
+
type: Object,
|
65 |
+
default: () => null
|
66 |
+
}
|
67 |
+
},
|
68 |
+
emits: ['data-loaded', 'error'],
|
69 |
+
setup(props, { emit }) {
|
70 |
+
const plotlyDiv = ref(null)
|
71 |
+
const loading = ref(false)
|
72 |
+
const error = ref(null)
|
73 |
+
const chartData = ref([])
|
74 |
+
|
75 |
+
// Fonction pour décompresser les données du CSV
|
76 |
+
const unpack = (rows, key) => {
|
77 |
+
return rows.map(row => row[key])
|
78 |
+
}
|
79 |
+
|
80 |
+
// Fonction pour charger les données depuis le CSV
|
81 |
+
const loadData = async () => {
|
82 |
+
loading.value = true
|
83 |
+
error.value = null
|
84 |
+
|
85 |
+
try {
|
86 |
+
if (props.dataType === 'football-field') {
|
87 |
+
// Générer les données du terrain de football
|
88 |
+
await generateFootballFieldData()
|
89 |
+
} else {
|
90 |
+
// Charger depuis CSV (comportement original)
|
91 |
+
await loadCsvData()
|
92 |
+
}
|
93 |
+
|
94 |
+
await renderChart()
|
95 |
+
emit('data-loaded', { traces: chartData.value })
|
96 |
+
|
97 |
+
} catch (err) {
|
98 |
+
console.error('Erreur lors du chargement des données:', err)
|
99 |
+
error.value = err.message
|
100 |
+
emit('error', err)
|
101 |
+
} finally {
|
102 |
+
loading.value = false
|
103 |
+
}
|
104 |
+
}
|
105 |
+
|
106 |
+
// Fonction pour charger les données CSV (ancien comportement)
|
107 |
+
const loadCsvData = async () => {
|
108 |
+
const response = await fetch(props.csvUrl)
|
109 |
+
if (!response.ok) {
|
110 |
+
throw new Error(`HTTP error! status: ${response.status}`)
|
111 |
+
}
|
112 |
+
|
113 |
+
const csvText = await response.text()
|
114 |
+
const rows = csvParse(csvText)
|
115 |
+
|
116 |
+
if (!rows || rows.length === 0) {
|
117 |
+
throw new Error('Aucune donnée trouvée dans le CSV')
|
118 |
+
}
|
119 |
+
|
120 |
+
// Création des traces comme dans votre exemple
|
121 |
+
const trace1 = {
|
122 |
+
x: unpack(rows, 'x1'),
|
123 |
+
y: unpack(rows, 'y1'),
|
124 |
+
z: unpack(rows, 'z1'),
|
125 |
+
mode: 'markers',
|
126 |
+
marker: {
|
127 |
+
size: 12,
|
128 |
+
line: {
|
129 |
+
color: 'rgba(217, 217, 217, 0.14)',
|
130 |
+
width: 0.5
|
131 |
+
},
|
132 |
+
opacity: 0.8
|
133 |
+
},
|
134 |
+
type: 'scatter3d',
|
135 |
+
name: 'Série 1'
|
136 |
+
}
|
137 |
+
|
138 |
+
const trace2 = {
|
139 |
+
x: unpack(rows, 'x2'),
|
140 |
+
y: unpack(rows, 'y2'),
|
141 |
+
z: unpack(rows, 'z2'),
|
142 |
+
mode: 'markers',
|
143 |
+
marker: {
|
144 |
+
color: 'rgb(127, 127, 127)',
|
145 |
+
size: 12,
|
146 |
+
symbol: 'circle',
|
147 |
+
line: {
|
148 |
+
color: 'rgb(204, 204, 204)',
|
149 |
+
width: 1
|
150 |
+
},
|
151 |
+
opacity: 0.8
|
152 |
+
},
|
153 |
+
type: 'scatter3d',
|
154 |
+
name: 'Série 2'
|
155 |
+
}
|
156 |
+
|
157 |
+
chartData.value = [trace1, trace2]
|
158 |
+
}
|
159 |
+
|
160 |
+
// Fonction pour générer les données du terrain de football
|
161 |
+
const generateFootballFieldData = async () => {
|
162 |
+
const fieldLength = 105 // mètres
|
163 |
+
const fieldWidth = 68 // mètres
|
164 |
+
|
165 |
+
const traces = []
|
166 |
+
|
167 |
+
// 1. Contour principal du terrain
|
168 |
+
const fieldCorners = {
|
169 |
+
x: [-fieldLength/2, fieldLength/2, fieldLength/2, -fieldLength/2, -fieldLength/2],
|
170 |
+
y: [-fieldWidth/2, -fieldWidth/2, fieldWidth/2, fieldWidth/2, -fieldWidth/2],
|
171 |
+
z: [0, 0, 0, 0, 0],
|
172 |
+
mode: 'lines',
|
173 |
+
type: 'scatter3d',
|
174 |
+
line: { color: '#00FF00', width: 6 },
|
175 |
+
name: 'Terrain principal',
|
176 |
+
showlegend: true
|
177 |
+
}
|
178 |
+
traces.push(fieldCorners)
|
179 |
+
|
180 |
+
// 2. Ligne médiane
|
181 |
+
const midline = {
|
182 |
+
x: [0, 0],
|
183 |
+
y: [-fieldWidth/2, fieldWidth/2],
|
184 |
+
z: [0, 0],
|
185 |
+
mode: 'lines',
|
186 |
+
type: 'scatter3d',
|
187 |
+
line: { color: 'bleu', width: 4 },
|
188 |
+
name: 'Ligne médiane',
|
189 |
+
showlegend: true
|
190 |
+
}
|
191 |
+
traces.push(midline)
|
192 |
+
|
193 |
+
// 3. Surface de réparation gauche
|
194 |
+
const leftPenaltyArea = {
|
195 |
+
x: [-fieldLength/2, -fieldLength/2+16.5, -fieldLength/2+16.5, -fieldLength/2, -fieldLength/2],
|
196 |
+
y: [-20.16, -20.16, 20.16, 20.16, -20.16],
|
197 |
+
z: [0, 0, 0, 0, 0],
|
198 |
+
mode: 'lines',
|
199 |
+
type: 'scatter3d',
|
200 |
+
line: { color: '#FF6B6B', width: 5 },
|
201 |
+
name: 'Surface de réparation',
|
202 |
+
showlegend: true
|
203 |
+
}
|
204 |
+
traces.push(leftPenaltyArea)
|
205 |
+
|
206 |
+
// 4. Surface de réparation droite
|
207 |
+
const rightPenaltyArea = {
|
208 |
+
x: [fieldLength/2, fieldLength/2-16.5, fieldLength/2-16.5, fieldLength/2, fieldLength/2],
|
209 |
+
y: [-20.16, -20.16, 20.16, 20.16, -20.16],
|
210 |
+
z: [0, 0, 0, 0, 0],
|
211 |
+
mode: 'lines',
|
212 |
+
type: 'scatter3d',
|
213 |
+
line: { color: '#FF6B6B', width: 5 },
|
214 |
+
name: 'Surface de réparation droite',
|
215 |
+
showlegend: false // Eviter la duplication dans la légende
|
216 |
+
}
|
217 |
+
traces.push(rightPenaltyArea)
|
218 |
+
|
219 |
+
// 5. Surface de but gauche (6 yards)
|
220 |
+
const leftGoalArea = {
|
221 |
+
x: [-fieldLength/2, -fieldLength/2+5.5, -fieldLength/2+5.5, -fieldLength/2, -fieldLength/2],
|
222 |
+
y: [-9.16, -9.16, 9.16, 9.16, -9.16],
|
223 |
+
z: [0, 0, 0, 0, 0],
|
224 |
+
mode: 'lines',
|
225 |
+
type: 'scatter3d',
|
226 |
+
line: { color: '#4ECDC4', width: 4 },
|
227 |
+
name: 'Surface de but',
|
228 |
+
showlegend: true
|
229 |
+
}
|
230 |
+
traces.push(leftGoalArea)
|
231 |
+
|
232 |
+
// 6. Surface de but droite
|
233 |
+
const rightGoalArea = {
|
234 |
+
x: [fieldLength/2, fieldLength/2-5.5, fieldLength/2-5.5, fieldLength/2, fieldLength/2],
|
235 |
+
y: [-9.16, -9.16, 9.16, 9.16, -9.16],
|
236 |
+
z: [0, 0, 0, 0, 0],
|
237 |
+
mode: 'lines',
|
238 |
+
type: 'scatter3d',
|
239 |
+
line: { color: '#4ECDC4', width: 4 },
|
240 |
+
showlegend: false
|
241 |
+
}
|
242 |
+
traces.push(rightGoalArea)
|
243 |
+
|
244 |
+
// 7. Cercle central
|
245 |
+
const circlePoints = 50
|
246 |
+
const radius = 9.15
|
247 |
+
const theta = Array.from({length: circlePoints}, (_, i) => (i / (circlePoints - 1)) * 2 * Math.PI)
|
248 |
+
const centerCircle = {
|
249 |
+
x: theta.map(t => radius * Math.cos(t)),
|
250 |
+
y: theta.map(t => radius * Math.sin(t)),
|
251 |
+
z: theta.map(() => 0),
|
252 |
+
mode: 'lines',
|
253 |
+
type: 'scatter3d',
|
254 |
+
line: { color: '#FFE66D', width: 4 },
|
255 |
+
name: 'Cercle central',
|
256 |
+
showlegend: true
|
257 |
+
}
|
258 |
+
traces.push(centerCircle)
|
259 |
+
|
260 |
+
// 8. Points de penalty
|
261 |
+
const penaltySpots = {
|
262 |
+
x: [-fieldLength/2 + 11, fieldLength/2 - 11],
|
263 |
+
y: [0, 0],
|
264 |
+
z: [0, 0],
|
265 |
+
mode: 'markers',
|
266 |
+
type: 'scatter3d',
|
267 |
+
marker: {
|
268 |
+
color: '#FFFFFF',
|
269 |
+
size: 8,
|
270 |
+
symbol: 'circle'
|
271 |
+
},
|
272 |
+
name: 'Points de penalty',
|
273 |
+
showlegend: true
|
274 |
+
}
|
275 |
+
traces.push(penaltySpots)
|
276 |
+
|
277 |
+
// 9. Point central
|
278 |
+
const centerSpot = {
|
279 |
+
x: [0],
|
280 |
+
y: [0],
|
281 |
+
z: [0],
|
282 |
+
mode: 'markers',
|
283 |
+
type: 'scatter3d',
|
284 |
+
marker: {
|
285 |
+
color: '#FFFFFF',
|
286 |
+
size: 6,
|
287 |
+
symbol: 'circle'
|
288 |
+
},
|
289 |
+
name: 'Point central',
|
290 |
+
showlegend: true
|
291 |
+
}
|
292 |
+
traces.push(centerSpot)
|
293 |
+
|
294 |
+
// 10. Keypoints détectés (si disponibles)
|
295 |
+
if (props.keypointsData && props.keypointsData.length > 0) {
|
296 |
+
const keypointsTrace = {
|
297 |
+
x: props.keypointsData.map(kp => kp.world_coords?.x || 0),
|
298 |
+
y: props.keypointsData.map(kp => kp.world_coords?.y || 0),
|
299 |
+
z: props.keypointsData.map(() => 0.5), // Légèrement au-dessus du terrain
|
300 |
+
mode: 'markers+text',
|
301 |
+
type: 'scatter3d',
|
302 |
+
marker: {
|
303 |
+
color: '#FF1744',
|
304 |
+
size: 6,
|
305 |
+
symbol: 'circle',
|
306 |
+
line: {
|
307 |
+
color: '#FFFFFF',
|
308 |
+
width: 2
|
309 |
+
}
|
310 |
+
},
|
311 |
+
text: props.keypointsData.map(kp => `KP ${kp.id}`),
|
312 |
+
textposition: 'top center',
|
313 |
+
textfont: {
|
314 |
+
color: 'black',
|
315 |
+
size: 8,
|
316 |
+
family: 'Arial, sans-serif'
|
317 |
+
},
|
318 |
+
name: 'Keypoints détectés',
|
319 |
+
showlegend: false,
|
320 |
+
hovertemplate:
|
321 |
+
'<b>Keypoint %{text}</b><br>' +
|
322 |
+
'X: %{x:.1f}m<br>' +
|
323 |
+
'Y: %{y:.1f}m<br>' +
|
324 |
+
'<extra></extra>'
|
325 |
+
}
|
326 |
+
traces.push(keypointsTrace)
|
327 |
+
}
|
328 |
+
|
329 |
+
// 11. Position de la caméra (si disponible)
|
330 |
+
if (props.cameraParams?.position_meters) {
|
331 |
+
const [camX, camY, camZ] = props.cameraParams.position_meters
|
332 |
+
|
333 |
+
const cameraTrace = {
|
334 |
+
x: [camX],
|
335 |
+
y: [camY],
|
336 |
+
z: [camZ],
|
337 |
+
mode: 'markers+text',
|
338 |
+
type: 'scatter3d',
|
339 |
+
marker: {
|
340 |
+
color: '#2196F3',
|
341 |
+
size: 15,
|
342 |
+
symbol: 'square',
|
343 |
+
line: {
|
344 |
+
color: '#FFFFFF',
|
345 |
+
width: 3
|
346 |
+
}
|
347 |
+
},
|
348 |
+
text: ['📷 Caméra'],
|
349 |
+
textposition: 'top center',
|
350 |
+
textfont: {
|
351 |
+
color: '#2196F3',
|
352 |
+
size: 12,
|
353 |
+
family: 'Arial, sans-serif'
|
354 |
+
},
|
355 |
+
name: 'Position caméra',
|
356 |
+
showlegend: false,
|
357 |
+
hovertemplate:
|
358 |
+
'<b>📷 Position de la caméra</b><br>' +
|
359 |
+
'X: %{x:.2f}m<br>' +
|
360 |
+
'Y: %{y:.2f}m<br>' +
|
361 |
+
'Z: %{z:.2f}m<br>' +
|
362 |
+
'<extra></extra>'
|
363 |
+
}
|
364 |
+
traces.push(cameraTrace)
|
365 |
+
|
366 |
+
// 12. Ligne de vue de la caméra vers le centre du terrain (optionnel)
|
367 |
+
const sightLineTrace = {
|
368 |
+
x: [camX, 0],
|
369 |
+
y: [camY, 0],
|
370 |
+
z: [camZ, 0],
|
371 |
+
mode: 'lines',
|
372 |
+
type: 'scatter3d',
|
373 |
+
line: {
|
374 |
+
color: '#2196F3',
|
375 |
+
width: 2,
|
376 |
+
dash: 'dot'
|
377 |
+
},
|
378 |
+
name: 'Ligne de vue',
|
379 |
+
showlegend: false,
|
380 |
+
hoverinfo: 'skip'
|
381 |
+
}
|
382 |
+
traces.push(sightLineTrace)
|
383 |
+
}
|
384 |
+
|
385 |
+
chartData.value = traces
|
386 |
+
}
|
387 |
+
|
388 |
+
// Fonction pour rendre le graphique
|
389 |
+
const renderChart = async () => {
|
390 |
+
if (!plotlyDiv.value || chartData.value.length === 0) return
|
391 |
+
|
392 |
+
let defaultLayout = {
|
393 |
+
margin: {
|
394 |
+
l: 0,
|
395 |
+
r: 0,
|
396 |
+
b: 0,
|
397 |
+
t: 0
|
398 |
+
},
|
399 |
+
height: typeof props.height === 'number' ? props.height : parseInt(props.height),
|
400 |
+
scene: {
|
401 |
+
xaxis: { title: 'X Axis' },
|
402 |
+
yaxis: { title: 'Y Axis' },
|
403 |
+
zaxis: { title: 'Z Axis' }
|
404 |
+
}
|
405 |
+
}
|
406 |
+
|
407 |
+
// Layout spécifique pour le terrain de football
|
408 |
+
if (props.dataType === 'football-field') {
|
409 |
+
const shift_l = 10;
|
410 |
+
const shift_w = 40;
|
411 |
+
|
412 |
+
// Ranges de base
|
413 |
+
let baseLength = 52.5 + shift_l; // range X: [-baseLength, baseLength]
|
414 |
+
let baseWidth = 34 + shift_w; // range Y: [-baseWidth, baseWidth]
|
415 |
+
let baseHeight = 35; // range Z: [-baseHeight, baseHeight]
|
416 |
+
|
417 |
+
// Position de la caméra
|
418 |
+
const camX = props.cameraParams?.position_meters?.[0] || 0;
|
419 |
+
const camY = props.cameraParams?.position_meters?.[1] || 0;
|
420 |
+
const camZ = props.cameraParams?.position_meters?.[2] || 0;
|
421 |
+
|
422 |
+
// Vérifier si la caméra dépasse les ranges et ajuster si nécessaire
|
423 |
+
const maxCamX = Math.abs(camX);
|
424 |
+
const maxCamY = Math.abs(camY);
|
425 |
+
const maxCamZ = Math.abs(camZ);
|
426 |
+
|
427 |
+
// Ratios actuels pour conserver les proportions
|
428 |
+
const ratioXY = baseLength / baseWidth; // ratio X/Y
|
429 |
+
const ratioXZ = baseLength / baseHeight; // ratio X/Z
|
430 |
+
const ratioYZ = baseWidth / baseHeight; // ratio Y/Z
|
431 |
+
|
432 |
+
// Ajuster les ranges si la caméra dépasse
|
433 |
+
if (maxCamX > baseLength) {
|
434 |
+
baseLength = maxCamX + 10; // marge de 10m
|
435 |
+
baseWidth = baseLength / ratioXY; // conserver ratio X/Y
|
436 |
+
baseHeight = baseLength / ratioXZ; // conserver ratio X/Z
|
437 |
+
}
|
438 |
+
|
439 |
+
if (maxCamY > baseWidth) {
|
440 |
+
baseWidth = maxCamY + 10; // marge de 10m
|
441 |
+
baseLength = baseWidth * ratioXY; // conserver ratio X/Y
|
442 |
+
baseHeight = baseWidth / ratioYZ; // conserver ratio Y/Z
|
443 |
+
}
|
444 |
+
|
445 |
+
if (maxCamZ > baseHeight) {
|
446 |
+
baseHeight = maxCamZ + 10; // marge de 10m
|
447 |
+
baseLength = baseHeight * ratioXZ; // conserver ratio X/Z
|
448 |
+
baseWidth = baseHeight * ratioYZ; // conserver ratio Y/Z
|
449 |
+
}
|
450 |
+
|
451 |
+
// Valeurs finales
|
452 |
+
const length = baseLength;
|
453 |
+
const witdh = baseWidth;
|
454 |
+
const height = baseHeight;
|
455 |
+
|
456 |
+
defaultLayout.scene = {
|
457 |
+
xaxis: {
|
458 |
+
title: '',
|
459 |
+
range: [-length, length],
|
460 |
+
showgrid: false,
|
461 |
+
showticklabels: false,
|
462 |
+
showline: false,
|
463 |
+
zeroline: false,
|
464 |
+
dtick: 20
|
465 |
+
},
|
466 |
+
yaxis: {
|
467 |
+
title: '',
|
468 |
+
range: [-witdh, witdh],
|
469 |
+
showgrid: false,
|
470 |
+
showticklabels: false,
|
471 |
+
showline: false,
|
472 |
+
zeroline: false,
|
473 |
+
dtick: 20
|
474 |
+
},
|
475 |
+
zaxis: {
|
476 |
+
title: '',
|
477 |
+
range: [-height, height],
|
478 |
+
showgrid: false,
|
479 |
+
showticklabels: false,
|
480 |
+
showline: false,
|
481 |
+
zeroline: false,
|
482 |
+
dtick: 0
|
483 |
+
},
|
484 |
+
aspectmode: 'manual',
|
485 |
+
aspectratio: { x: 1., y: 1, z: 0.3 },
|
486 |
+
camera: {
|
487 |
+
eye: { x: 0, y: 1, z: -0.6 },
|
488 |
+
center: { x: 0, y: 0, z: 0 },
|
489 |
+
up: { x: 0, y: -1, z: 0 }
|
490 |
+
}
|
491 |
+
}
|
492 |
+
defaultLayout.margin.t = 10
|
493 |
+
defaultLayout.showlegend = false
|
494 |
+
}
|
495 |
+
|
496 |
+
const layout = {
|
497 |
+
...defaultLayout,
|
498 |
+
...props.customLayout
|
499 |
+
}
|
500 |
+
|
501 |
+
const config = {
|
502 |
+
responsive: true,
|
503 |
+
displayModeBar: true,
|
504 |
+
modeBarButtonsToRemove: ['pan2d', 'lasso2d'],
|
505 |
+
displaylogo: false
|
506 |
+
}
|
507 |
+
|
508 |
+
await nextTick()
|
509 |
+
await Plotly.newPlot(plotlyDiv.value, chartData.value, layout, config)
|
510 |
+
}
|
511 |
+
|
512 |
+
// Fonction de nettoyage
|
513 |
+
const cleanup = () => {
|
514 |
+
if (plotlyDiv.value) {
|
515 |
+
Plotly.purge(plotlyDiv.value)
|
516 |
+
}
|
517 |
+
}
|
518 |
+
|
519 |
+
// Fonction pour redimensionner le graphique
|
520 |
+
const resizeChart = () => {
|
521 |
+
if (plotlyDiv.value) {
|
522 |
+
Plotly.Plots.resize(plotlyDiv.value)
|
523 |
+
}
|
524 |
+
}
|
525 |
+
|
526 |
+
// Watcher pour recharger quand les keypoints changent
|
527 |
+
watch(() => props.keypointsData, () => {
|
528 |
+
if (props.dataType === 'football-field') {
|
529 |
+
loadData()
|
530 |
+
}
|
531 |
+
}, { deep: true })
|
532 |
+
|
533 |
+
// Watcher pour recharger quand les paramètres de la caméra changent
|
534 |
+
watch(() => props.cameraParams, () => {
|
535 |
+
if (props.dataType === 'football-field') {
|
536 |
+
loadData()
|
537 |
+
}
|
538 |
+
}, { deep: true })
|
539 |
+
|
540 |
+
// Lifecycle hooks
|
541 |
+
onMounted(() => {
|
542 |
+
loadData()
|
543 |
+
window.addEventListener('resize', resizeChart)
|
544 |
+
})
|
545 |
+
|
546 |
+
onUnmounted(() => {
|
547 |
+
cleanup()
|
548 |
+
window.removeEventListener('resize', resizeChart)
|
549 |
+
})
|
550 |
+
|
551 |
+
return {
|
552 |
+
plotlyDiv,
|
553 |
+
loading,
|
554 |
+
error,
|
555 |
+
loadData,
|
556 |
+
resizeChart
|
557 |
+
}
|
558 |
+
}
|
559 |
+
}
|
560 |
+
</script>
|
561 |
+
|
562 |
+
<style scoped>
|
563 |
+
.plotly-chart {
|
564 |
+
background: white;
|
565 |
+
border-radius: 8px;
|
566 |
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
567 |
+
overflow: hidden;
|
568 |
+
}
|
569 |
+
|
570 |
+
|
571 |
+
|
572 |
+
.chart-container {
|
573 |
+
position: relative;
|
574 |
+
min-height: 200px;
|
575 |
+
}
|
576 |
+
|
577 |
+
.chart-container.loading {
|
578 |
+
display: flex;
|
579 |
+
align-items: center;
|
580 |
+
justify-content: center;
|
581 |
+
}
|
582 |
+
|
583 |
+
.loading-spinner {
|
584 |
+
display: flex;
|
585 |
+
flex-direction: column;
|
586 |
+
align-items: center;
|
587 |
+
gap: 1rem;
|
588 |
+
}
|
589 |
+
|
590 |
+
.spinner {
|
591 |
+
width: 40px;
|
592 |
+
height: 40px;
|
593 |
+
border: 4px solid #f3f3f3;
|
594 |
+
border-top: 4px solid #007bff;
|
595 |
+
border-radius: 50%;
|
596 |
+
animation: spin 1s linear infinite;
|
597 |
+
}
|
598 |
+
|
599 |
+
@keyframes spin {
|
600 |
+
0% { transform: rotate(0deg); }
|
601 |
+
100% { transform: rotate(360deg); }
|
602 |
+
}
|
603 |
+
|
604 |
+
.error-message {
|
605 |
+
text-align: center;
|
606 |
+
padding: 2rem;
|
607 |
+
color: #dc3545;
|
608 |
+
}
|
609 |
+
|
610 |
+
.retry-btn {
|
611 |
+
background: #dc3545;
|
612 |
+
color: white;
|
613 |
+
border: none;
|
614 |
+
padding: 0.5rem 1rem;
|
615 |
+
border-radius: 4px;
|
616 |
+
cursor: pointer;
|
617 |
+
margin-top: 1rem;
|
618 |
+
}
|
619 |
+
|
620 |
+
.retry-btn:hover {
|
621 |
+
background: #c82333;
|
622 |
+
}
|
623 |
+
|
624 |
+
/* Correction de l'alignement de la modebar Plotly */
|
625 |
+
.plotly-chart :deep(.modebar) {
|
626 |
+
display: flex !important;
|
627 |
+
align-items: center !important;
|
628 |
+
justify-content: flex-end !important;
|
629 |
+
padding: 5px !important;
|
630 |
+
background: none !important;
|
631 |
+
}
|
632 |
+
|
633 |
+
.plotly-chart :deep(.modebar-group) {
|
634 |
+
display: flex !important;
|
635 |
+
align-items: center !important;
|
636 |
+
margin: 0 2px !important;
|
637 |
+
background: none !important;
|
638 |
+
border: none !important;
|
639 |
+
}
|
640 |
+
|
641 |
+
.plotly-chart :deep(.modebar-btn) {
|
642 |
+
display: flex !important;
|
643 |
+
align-items: center !important;
|
644 |
+
justify-content: center !important;
|
645 |
+
margin: 0 1px !important;
|
646 |
+
background: none !important;
|
647 |
+
border: none !important;
|
648 |
+
}
|
649 |
+
|
650 |
+
/* Personnalisation des icônes */
|
651 |
+
.plotly-chart :deep(.modebar-btn .icon path) {
|
652 |
+
fill: black !important;
|
653 |
+
}
|
654 |
+
|
655 |
+
.plotly-chart :deep(.modebar-btn:hover) {
|
656 |
+
background: rgba(0, 0, 0, 0.1) !important;
|
657 |
+
}
|
658 |
+
|
659 |
+
.plotly-chart :deep(.modebar-btn.active) {
|
660 |
+
background: rgba(0, 0, 0, 0.2) !important;
|
661 |
+
}
|
662 |
+
|
663 |
+
/* Responsive */
|
664 |
+
@media (max-width: 768px) {
|
665 |
+
.chart-header {
|
666 |
+
flex-direction: column;
|
667 |
+
gap: 0.5rem;
|
668 |
+
text-align: center;
|
669 |
+
}
|
670 |
+
|
671 |
+
.chart-container {
|
672 |
+
min-height: 300px;
|
673 |
+
}
|
674 |
+
}
|
675 |
+
</style>
|
src/services/api.js
CHANGED
@@ -1,5 +1,5 @@
|
|
1 |
-
|
2 |
-
const API_BASE_URL = 'https://2nzi-pnlcalib.hf.space'
|
3 |
|
4 |
class FootballVisionAPI {
|
5 |
constructor() {
|
|
|
1 |
+
const API_BASE_URL = 'http://localhost:8000'
|
2 |
+
// const API_BASE_URL = 'https://2nzi-pnlcalib.hf.space'
|
3 |
|
4 |
class FootballVisionAPI {
|
5 |
constructor() {
|
src/views/HomeView.vue
CHANGED
@@ -6,6 +6,7 @@ import { useUploadStore } from '../stores/upload'
|
|
6 |
import api from '../services/api'
|
7 |
import CalibrationArea from '@/components/CalibrationArea.vue'
|
8 |
import FootballField from '@/components/FootballField.vue'
|
|
|
9 |
|
10 |
const router = useRouter()
|
11 |
const calibrationStore = useCalibrationStore()
|
@@ -525,10 +526,28 @@ const processCalibration = async () => {
|
|
525 |
})
|
526 |
}
|
527 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
528 |
</script>
|
529 |
|
530 |
<template>
|
531 |
-
|
|
|
|
|
|
|
532 |
<!-- Bouton retour subtil (visible si on a déjà fait des actions) -->
|
533 |
<button
|
534 |
v-if="showUpload || showResults || showError"
|
@@ -858,6 +877,27 @@ const processCalibration = async () => {
|
|
858 |
</button>
|
859 |
</div>
|
860 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
861 |
<details class="result-details">
|
862 |
<summary>Données complètes</summary>
|
863 |
<pre class="result-data">{{ JSON.stringify(calibrationStore.results, null, 2) }}</pre>
|
@@ -1454,7 +1494,7 @@ const processCalibration = async () => {
|
|
1454 |
|
1455 |
/* Section Résultats */
|
1456 |
.results-container {
|
1457 |
-
max-width:
|
1458 |
width: 100%;
|
1459 |
display: flex;
|
1460 |
flex-direction: column;
|
@@ -1560,19 +1600,27 @@ const processCalibration = async () => {
|
|
1560 |
}
|
1561 |
|
1562 |
.result-data {
|
1563 |
-
background: #
|
1564 |
-
color: #
|
1565 |
-
padding:
|
1566 |
-
border-radius:
|
1567 |
-
font-family: '
|
1568 |
-
font-size: 0.
|
1569 |
-
line-height: 1.
|
1570 |
overflow-x: auto;
|
1571 |
-
white-space: pre
|
1572 |
-
|
1573 |
-
|
1574 |
-
max-height: 300px;
|
1575 |
overflow-y: auto;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1576 |
}
|
1577 |
|
1578 |
.confidence-score {
|
@@ -1807,6 +1855,25 @@ const processCalibration = async () => {
|
|
1807 |
height: calc(100% - 80px);
|
1808 |
}
|
1809 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1810 |
/* Responsive */
|
1811 |
@media (max-width: 768px) {
|
1812 |
.home-container {
|
@@ -1833,5 +1900,9 @@ const processCalibration = async () => {
|
|
1833 |
flex-direction: column;
|
1834 |
}
|
1835 |
|
|
|
|
|
|
|
|
|
1836 |
}
|
1837 |
</style>
|
|
|
6 |
import api from '../services/api'
|
7 |
import CalibrationArea from '@/components/CalibrationArea.vue'
|
8 |
import FootballField from '@/components/FootballField.vue'
|
9 |
+
import PlotlyChart from '@/components/PlotlyChart.vue'
|
10 |
|
11 |
const router = useRouter()
|
12 |
const calibrationStore = useCalibrationStore()
|
|
|
526 |
})
|
527 |
}
|
528 |
}
|
529 |
+
|
530 |
+
// Méthodes pour les événements du graphique Plotly
|
531 |
+
const onChartDataLoaded = (data) => {
|
532 |
+
if (data.rows) {
|
533 |
+
// Pour les données CSV
|
534 |
+
console.log('📊 Données CSV chargées:', data.rows.length, 'points')
|
535 |
+
} else {
|
536 |
+
// Pour le terrain de football ou autres
|
537 |
+
console.log('📊 Graphique chargé:', data.traces.length, 'éléments')
|
538 |
+
}
|
539 |
+
}
|
540 |
+
|
541 |
+
const onChartError = (error) => {
|
542 |
+
console.error('❌ Erreur dans le graphique:', error)
|
543 |
+
}
|
544 |
</script>
|
545 |
|
546 |
<template>
|
547 |
+
|
548 |
+
|
549 |
+
|
550 |
+
<div class="home-container">
|
551 |
<!-- Bouton retour subtil (visible si on a déjà fait des actions) -->
|
552 |
<button
|
553 |
v-if="showUpload || showResults || showError"
|
|
|
877 |
</button>
|
878 |
</div>
|
879 |
|
880 |
+
<!-- Graphique 3D pour les images avec calibration réussie -->
|
881 |
+
<div v-if="uploadStore.isImage" class="result-chart">
|
882 |
+
<h3>Terrain de Football - Vue 3D</h3>
|
883 |
+
<PlotlyChart
|
884 |
+
data-type="football-field"
|
885 |
+
:height="550"
|
886 |
+
:keypoints-data="calibrationStore.results?.detections?.keypoints || []"
|
887 |
+
:camera-params="calibrationStore.results?.camera_parameters?.cam_params"
|
888 |
+
:custom-layout="{
|
889 |
+
title: {
|
890 |
+
text: 'Visualisation 3D du terrain de football',
|
891 |
+
font: { size: 16, color: 'white' }
|
892 |
+
},
|
893 |
+
paper_bgcolor: 'rgba(0,0,0,0)',
|
894 |
+
plot_bgcolor: 'rgba(0,0,0,0)'
|
895 |
+
}"
|
896 |
+
@data-loaded="onChartDataLoaded"
|
897 |
+
@error="onChartError"
|
898 |
+
/>
|
899 |
+
</div>
|
900 |
+
|
901 |
<details class="result-details">
|
902 |
<summary>Données complètes</summary>
|
903 |
<pre class="result-data">{{ JSON.stringify(calibrationStore.results, null, 2) }}</pre>
|
|
|
1494 |
|
1495 |
/* Section Résultats */
|
1496 |
.results-container {
|
1497 |
+
max-width: 800px;
|
1498 |
width: 100%;
|
1499 |
display: flex;
|
1500 |
flex-direction: column;
|
|
|
1600 |
}
|
1601 |
|
1602 |
.result-data {
|
1603 |
+
background: #0d1117;
|
1604 |
+
color: #e6edf3;
|
1605 |
+
padding: 1.5rem;
|
1606 |
+
border-radius: 8px;
|
1607 |
+
font-family: 'Monaco', 'Consolas', 'Ubuntu Mono', monospace;
|
1608 |
+
font-size: 0.875rem;
|
1609 |
+
line-height: 1.6;
|
1610 |
overflow-x: auto;
|
1611 |
+
white-space: pre;
|
1612 |
+
border: 1px solid #30363d;
|
1613 |
+
max-height: 400px;
|
|
|
1614 |
overflow-y: auto;
|
1615 |
+
text-align: left;
|
1616 |
+
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.3);
|
1617 |
+
|
1618 |
+
/* Syntax highlighting simulation */
|
1619 |
+
color: #c9d1d9;
|
1620 |
+
}
|
1621 |
+
|
1622 |
+
.result-details {
|
1623 |
+
position: relative;
|
1624 |
}
|
1625 |
|
1626 |
.confidence-score {
|
|
|
1855 |
height: calc(100% - 80px);
|
1856 |
}
|
1857 |
|
1858 |
+
/* Section graphique dans les résultats */
|
1859 |
+
.result-chart {
|
1860 |
+
width: 100%;
|
1861 |
+
max-width: 1000px;
|
1862 |
+
margin: 2rem 0;
|
1863 |
+
background: rgba(255, 255, 255, 0.02);
|
1864 |
+
border-radius: 12px;
|
1865 |
+
padding: 1.5rem;
|
1866 |
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
1867 |
+
}
|
1868 |
+
|
1869 |
+
.result-chart h3 {
|
1870 |
+
font-size: 1.25rem;
|
1871 |
+
color: white;
|
1872 |
+
margin-bottom: 1rem;
|
1873 |
+
text-align: center;
|
1874 |
+
font-weight: 600;
|
1875 |
+
}
|
1876 |
+
|
1877 |
/* Responsive */
|
1878 |
@media (max-width: 768px) {
|
1879 |
.home-container {
|
|
|
1900 |
flex-direction: column;
|
1901 |
}
|
1902 |
|
1903 |
+
.result-chart {
|
1904 |
+
padding: 1rem;
|
1905 |
+
margin: 1rem 0;
|
1906 |
+
}
|
1907 |
}
|
1908 |
</style>
|