скрыть начало и конец
Gitea Actions Demo / build_and_push (push) Successful in 1m18s
Details
Gitea Actions Demo / build_and_push (push) Successful in 1m18s
Details
This commit is contained in:
parent
ad9e1a5bbf
commit
e59523fd1c
|
|
@ -2,7 +2,14 @@
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"playwright": {
|
"playwright": {
|
||||||
"command": "/home/artem/.nvm/versions/node/v22.14.0/bin/npx",
|
"command": "/home/artem/.nvm/versions/node/v22.14.0/bin/npx",
|
||||||
"args": ["@playwright/mcp@latest", "--browser", "chromium", "--headless", "--viewport-size", "1440x900"],
|
"args": [
|
||||||
|
"@playwright/mcp@latest",
|
||||||
|
"--browser",
|
||||||
|
"chromium",
|
||||||
|
"--headless",
|
||||||
|
"--viewport-size",
|
||||||
|
"1440x900"
|
||||||
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"PATH": "/home/artem/.nvm/versions/node/v22.14.0/bin:/usr/local/bin:/usr/bin:/bin"
|
"PATH": "/home/artem/.nvm/versions/node/v22.14.0/bin:/usr/local/bin:/usr/bin:/bin"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ yarn dev
|
||||||
### 2. Проверка публичных страниц (без auth)
|
### 2. Проверка публичных страниц (без auth)
|
||||||
|
|
||||||
| Рут | Что проверять |
|
| Рут | Что проверять |
|
||||||
|-----|---------------|
|
| ---------------------- | --------------------------------------------- |
|
||||||
| `/explore` | Feed загружен, карточки тренировок рендерятся |
|
| `/explore` | Feed загружен, карточки тренировок рендерятся |
|
||||||
| `/public/workouts/:id` | Детали тренировки: chart, map, данные |
|
| `/public/workouts/:id` | Детали тренировки: chart, map, данные |
|
||||||
| `/routes` | Список маршрутов |
|
| `/routes` | Список маршрутов |
|
||||||
|
|
@ -36,17 +36,23 @@ yarn dev
|
||||||
```js
|
```js
|
||||||
// browser_evaluate: установить мок-токен и перезагрузить
|
// browser_evaluate: установить мок-токен и перезагрузить
|
||||||
() => {
|
() => {
|
||||||
localStorage.setItem('token', 'mock-jwt-token');
|
localStorage.setItem("token", "mock-jwt-token");
|
||||||
localStorage.setItem('user', JSON.stringify({ id: 1, name: 'Test User', email: 'test@test.ru' }));
|
localStorage.setItem(
|
||||||
localStorage.setItem('profile', JSON.stringify({ name: 'Test', avatar: null }));
|
"user",
|
||||||
|
JSON.stringify({ id: 1, name: "Test User", email: "test@test.ru" }),
|
||||||
|
);
|
||||||
|
localStorage.setItem(
|
||||||
|
"profile",
|
||||||
|
JSON.stringify({ name: "Test", avatar: null }),
|
||||||
|
);
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
После reload:
|
После reload:
|
||||||
|
|
||||||
| Рут | Что проверять |
|
| Рут | Что проверять |
|
||||||
|-----|---------------|
|
| ------------------ | -------------------------------------------------- |
|
||||||
| `/workouts` | Список пользовательских тренировок |
|
| `/workouts` | Список пользовательских тренировок |
|
||||||
| `/workouts/:id` | Detail: chart (power, HR, elevation), map polyline |
|
| `/workouts/:id` | Detail: chart (power, HR, elevation), map polyline |
|
||||||
| `/preferences` | Профиль, настройки |
|
| `/preferences` | Профиль, настройки |
|
||||||
|
|
@ -55,7 +61,7 @@ yarn dev
|
||||||
### 4. Инструменты MCP (ключевые)
|
### 4. Инструменты MCP (ключевые)
|
||||||
|
|
||||||
| Инструмент | Назначение |
|
| Инструмент | Назначение |
|
||||||
|-----------|-----------|
|
| -------------------------- | ------------------------------------------- |
|
||||||
| `browser_navigate` | Перейти по URL |
|
| `browser_navigate` | Перейти по URL |
|
||||||
| `browser_snapshot` | Получить accessibility tree (структура DOM) |
|
| `browser_snapshot` | Получить accessibility tree (структура DOM) |
|
||||||
| `browser_take_screenshot` | Скриншот (PNG) |
|
| `browser_take_screenshot` | Скриншот (PNG) |
|
||||||
|
|
@ -95,6 +101,7 @@ yarn dev
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
### Acceptance Criteria
|
### Acceptance Criteria
|
||||||
|
|
||||||
1. `yarn lint` → 0 ошибок
|
1. `yarn lint` → 0 ошибок
|
||||||
2. `yarn build` → 0 ошибок (vue-tsc --noEmit)
|
2. `yarn build` → 0 ошибок (vue-tsc --noEmit)
|
||||||
3. MCP browser: [конкретный сценарий проверки — рут, что ожидать в DOM]
|
3. MCP browser: [конкретный сценарий проверки — рут, что ожидать в DOM]
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,37 @@
|
||||||
|
|
||||||
## Task State
|
## Task State
|
||||||
|
|
||||||
- task_id: TASK-TEST-PUBLIC
|
- task_id: TASK-HIDE-TRACK-T1
|
||||||
- status: in_progress
|
- status: success
|
||||||
- parent_task: —
|
- parent_task: TASK-HIDE-TRACK
|
||||||
- summary: **Написать Playwright integration тесты для фронтенда в `../integration/tests/`. 3 spec файла: auth-flows ✅, public-pages, auth-protected.**
|
- summary: **Backend тест `test_workout_hide_start_end` добавлен. Все 4 теста PASSED (5.13s).**
|
||||||
- next task: TASK-TEST-PROTECTED → Verify
|
- next task: —
|
||||||
|
|
||||||
|
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-T1
|
||||||
|
|
||||||
|
- `../backend/tests/test_api_workout.py`: добавлен `test_workout_hide_start_end` — проверяет PATCH `hide_start_end` (False→True→False) с GET-верификацией на каждом шаге.
|
||||||
|
- Verified: `./venv/bin/python -m pytest tests/test_api_workout.py::test_workout_hide_start_end -v` → **PASSED (1.92s)**
|
||||||
|
- Verified: `./venv/bin/python -m pytest tests/test_api_workout.py -v` → **4 passed (5.13s)**
|
||||||
|
|
||||||
|
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-B3
|
||||||
|
|
||||||
|
- `../backend/app/web/v0/workout.py`: `WorkoutEditReq` + поле `hide_start_end: bool | None = Field(None)`; endpoint передаёт `hide_start_end=req.hide_start_end` в `workout_service.workout_edit(...)`.
|
||||||
|
- `../backend/app/modules/charts/services/workout.py`: `workout_edit` + параметр `hide_start_end: bool | None = None`; тело: `if hide_start_end is not None: params["hide_start_end"] = hide_start_end`.
|
||||||
|
- Verified: `./venv/bin/python -c "from app.web.v0.workout import workout_update; print('OK')"` → OK.
|
||||||
|
- Verified: `./venv/bin/python -m mypy app/web/v0/workout.py app/modules/charts/services/workout.py --explicit-package-bases --no-error-summary` → 0 errors.
|
||||||
|
|
||||||
|
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-B2
|
||||||
|
|
||||||
|
- `../backend/migration/versions/2025-09-25-0000-a1b2c3d4e5f6-hide-start-end.py` создан: `revision='a1b2c3d4e5f6'`, `down_revision='b1c2d3e4f5a6'`; `upgrade()` → `op.add_column('workouts', sa.Column('hide_start_end', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False))`; `downgrade()` → `op.drop_column(...)`. Без `IS_TEST`-guard (колонка нужна и в тестах).
|
||||||
|
- Verified: `python3 -c "import ast; ast.parse(...)"` → OK.
|
||||||
|
|
||||||
|
## ✅ CODER SUCCESS REPORT — TASK-HIDE-TRACK-4
|
||||||
|
|
||||||
|
- `src/pages/workouts/WorkoutPublicItem.vue`: импорт `trimTrack`; в `.then()` при `d.workoutItem.hide_start_end === true` — обрезка `lineCoordinates`/`distances`/`data` через `trimTrack(..., totalDist)` (totalDist = последний элемент `distances`). `mapCenter` остаётся неизменным.
|
||||||
|
- `src/pages/workouts/Definitions.vue`: добавлено поле `hide_start_end: boolean` в тип `WorkoutItem` (восстановлено — было потеряно при git stash).
|
||||||
|
- Приватный вид (`WorkoutItem.vue` страница) НЕ затронут.
|
||||||
|
- Verified: `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite ✓ 6.86s).
|
||||||
|
- Note: `yarn lint` → системная ошибка `tsutils.iterateComments is not a function` (касается ВСЕХ .vue файлов, не связано с изменениями — существующий дефект окружения lint-депенденций).
|
||||||
|
|
||||||
## ✅ CODER SUCCESS REPORT — TASK-TEST-AUTH
|
## ✅ CODER SUCCESS REPORT — TASK-TEST-AUTH
|
||||||
|
|
||||||
|
|
@ -21,6 +47,7 @@
|
||||||
**Файл для создания:** `../integration/tests/auth-flows.spec.ts`
|
**Файл для создания:** `../integration/tests/auth-flows.spec.ts`
|
||||||
|
|
||||||
**Конвенции (см. SKILL.md):**
|
**Конвенции (см. SKILL.md):**
|
||||||
|
|
||||||
- Self-seed user via `POST /api/v0/signup` in `beforeAll`
|
- Self-seed user via `POST /api/v0/signup` in `beforeAll`
|
||||||
- Selectors: `input[type=email]`, `input[type=password]`, `getByRole('button', { name: 'Вход' })`
|
- Selectors: `input[type=email]`, `input[type=password]`, `getByRole('button', { name: 'Вход' })`
|
||||||
- Vuestic floating labels → select by input type, NOT placeholder
|
- Vuestic floating labels → select by input type, NOT placeholder
|
||||||
|
|
@ -30,12 +57,14 @@
|
||||||
- `freshPage()` helper: goto → clear localStorage → goto again
|
- `freshPage()` helper: goto → clear localStorage → goto again
|
||||||
|
|
||||||
**Тесты в spec:**
|
**Тесты в spec:**
|
||||||
|
|
||||||
1. **Signup**: `POST /api/v0/signup` → returns token, user, profile; navigate to `/auth/signup` → fill form → submit → redirect to `/explore`
|
1. **Signup**: `POST /api/v0/signup` → returns token, user, profile; navigate to `/auth/signup` → fill form → submit → redirect to `/explore`
|
||||||
2. **Login success**: pre-seed user via API → UI login → token in localStorage → redirect to `/explore`
|
2. **Login success**: pre-seed user via API → UI login → token in localStorage → redirect to `/explore`
|
||||||
3. **Login failure**: wrong password → error toast appears, stays on `/auth/login`
|
3. **Login failure**: wrong password → error toast appears, stays on `/auth/login`
|
||||||
4. **Logout**: logged in → open profile dropdown → «Выход» → redirect to login, token cleared
|
4. **Logout**: logged in → open profile dropdown → «Выход» → redirect to login, token cleared
|
||||||
|
|
||||||
**Acceptance Criteria:**
|
**Acceptance Criteria:**
|
||||||
|
|
||||||
1. Spec file created at `../integration/tests/auth-flows.spec.ts`
|
1. Spec file created at `../integration/tests/auth-flows.spec.ts`
|
||||||
2. All tests self-contained (self-seed data)
|
2. All tests self-contained (self-seed data)
|
||||||
3. Follows existing `sidebar-auth-switch.spec.ts` patterns
|
3. Follows existing `sidebar-auth-switch.spec.ts` patterns
|
||||||
|
|
@ -44,12 +73,14 @@
|
||||||
## TASK-MCP-SETUP: Playwright MCP configuration
|
## TASK-MCP-SETUP: Playwright MCP configuration
|
||||||
|
|
||||||
**Что сделано:**
|
**Что сделано:**
|
||||||
|
|
||||||
- MCP-конфиг Zoo Code (`mcp_settings.json`): server `web-browser` → `@playwright/mcp@latest`, Node 22 via nvm absolute path, `--headless`
|
- MCP-конфиг Zoo Code (`mcp_settings.json`): server `web-browser` → `@playwright/mcp@latest`, Node 22 via nvm absolute path, `--headless`
|
||||||
- Chromium `1234` уже установлен в `~/.cache/ms-playwright/`
|
- Chromium `1234` уже установлен в `~/.cache/ms-playwright/`
|
||||||
- `.mcp.json` создан в проекте (project-level, для совместимости)
|
- `.mcp.json` создан в проекте (project-level, для совместимости)
|
||||||
- `techContext.md` обновлён: секция "UI Verification (MCP Browser)"
|
- `techContext.md` обновлён: секция "UI Verification (MCP Browser)"
|
||||||
|
|
||||||
**Как использовать (workflow):**
|
**Как использовать (workflow):**
|
||||||
|
|
||||||
1. `yarn dev` — запустить dev server (порт 5173)
|
1. `yarn dev` — запустить dev server (порт 5173)
|
||||||
2. `browser_navigate` → `http://localhost:5173/explore` — проверить публичную страницу
|
2. `browser_navigate` → `http://localhost:5173/explore` — проверить публичную страницу
|
||||||
3. `browser_evaluate` → `() => { localStorage.setItem('token', 'test'); localStorage.setItem('user', JSON.stringify({id:1,name:'Test'})); location.reload(); }` — сымитировать auth
|
3. `browser_evaluate` → `() => { localStorage.setItem('token', 'test'); localStorage.setItem('user', JSON.stringify({id:1,name:'Test'})); location.reload(); }` — сымитировать auth
|
||||||
|
|
@ -104,6 +135,8 @@
|
||||||
|
|
||||||
## История (закрытые задачи)
|
## История (закрытые задачи)
|
||||||
|
|
||||||
|
- **TASK-HIDE-TRACK-4**: success — `trimTrack` в `WorkoutPublicItem.vue` при `hide_start_end`; `hide_start_end` в типе `WorkoutItem` (Definitions.vue). Verified: `yarn build` 0. (Пред. active: TASK-TEST-PUBLIC → в очереди TASK-TEST-PROTECTED.)
|
||||||
|
|
||||||
- **TASK-AUTH-NAV-1**: success — `isAuthenticated` + `setAuthenticated` в `useGlobalStore`; Login `setAuthenticated(true)`, Logout `setAuthenticated(false)`. Verified lint/build 0.
|
- **TASK-AUTH-NAV-1**: success — `isAuthenticated` + `setAuthenticated` в `useGlobalStore`; Login `setAuthenticated(true)`, Logout `setAuthenticated(false)`. Verified lint/build 0.
|
||||||
- TASK-SSR-DETAIL: success — SSR /public/workouts/:id под DOM WorkoutItem.vue.
|
- TASK-SSR-DETAIL: success — SSR /public/workouts/:id под DOM WorkoutItem.vue.
|
||||||
- TASK-404: success — HTTP 404 для неизвестных страниц.
|
- TASK-404: success — HTTP 404 для неизвестных страниц.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,24 @@
|
||||||
# Progress — Strava Frontend
|
# Progress — Strava Frontend
|
||||||
|
|
||||||
|
## 2026-09-25 — TASK-HIDE-TRACK-B3: hide_start_end в PATCH endpoint + service (backend)
|
||||||
|
|
||||||
|
- `../backend/app/web/v0/workout.py` — `WorkoutEditReq` + `hide_start_end: bool | None = Field(None)`; PATCH `/workouts/{workout_id}` передаёт `hide_start_end=req.hide_start_end` в `workout_service.workout_edit(...)`.
|
||||||
|
- `../backend/app/modules/charts/services/workout.py` — `workout_edit` + параметр `hide_start_end: bool | None = None`; `if hide_start_end is not None: params["hide_start_end"] = hide_start_end` перед `repository.update`.
|
||||||
|
- Verified: `./venv/bin/python -c "from app.web.v0.workout import workout_update; print('OK')"` → OK.
|
||||||
|
- Verified: `./venv/bin/python -m mypy app/web/v0/workout.py app/modules/charts/services/workout.py --explicit-package-bases --no-error-summary` → 0 errors.
|
||||||
|
|
||||||
|
## 2026-09-25 — TASK-HIDE-TRACK-B2: Alembic migration hide_start_end (backend)
|
||||||
|
|
||||||
|
- `../backend/migration/versions/2025-09-25-0000-a1b2c3d4e5f6-hide-start-end.py` создан: `revision='a1b2c3d4e5f6'`, `down_revision='b1c2d3e4f5a6'`; `upgrade()` — `op.add_column('workouts', sa.Column('hide_start_end', sa.Boolean(), server_default=sa.text('FALSE'), nullable=False))`; `downgrade()` — `op.drop_column('workouts', 'hide_start_end')`. Без `IS_TEST`-guard (это колонка, не index/FK — нужна в тестовой БД тоже).
|
||||||
|
- Verified: `python3 -c "import ast; ast.parse(open(...).read())"` → OK (exit 0).
|
||||||
|
|
||||||
|
## 2026-09-25 — TASK-HIDE-TRACK-4: trimTrack в публичном виде тренировки
|
||||||
|
|
||||||
|
- `src/pages/workouts/WorkoutPublicItem.vue` — импорт `trimTrack` (`./components/TrimTrack`); в `.then()` `initWorkout`: при `d.workoutItem.hide_start_end` → `trimTrack(d.lineCoordinates, d.distances, d.data, totalDist)`, где `totalDist = d.distances[d.distances.length - 1] ?? 0`; иначе значения присваиваются как есть. `mapCenter` не меняется. Приватный вид не затронут.
|
||||||
|
- `src/pages/workouts/Definitions.vue` — `WorkoutItem` + поле `hide_start_end: boolean` (восстановлено после потери в git stash).
|
||||||
|
- Verified: `yarn build` exit 0 (vue-tsc 0 errors, vite ✓ 6.86s).
|
||||||
|
- Known issue: `yarn lint` сломан системно — `tsutils.iterateComments is not a function` на всех .vue файлах (дефект окружения, не от этих изменений).
|
||||||
|
|
||||||
## 2026-09-24 — UI-testing skill + mandatory rule
|
## 2026-09-24 — UI-testing skill + mandatory rule
|
||||||
|
|
||||||
- `.roo/skills/ui-testing/SKILL.md` — how to test UI via (A) MCP browser ad-hoc and (B) the Playwright integration stand: run commands, spec conventions, selectors, gotchas.
|
- `.roo/skills/ui-testing/SKILL.md` — how to test UI via (A) MCP browser ad-hoc and (B) the Playwright integration stand: run commands, spec conventions, selectors, gotchas.
|
||||||
|
|
@ -214,3 +233,9 @@
|
||||||
- `src/pages/auth/Login.vue` (2×), `src/pages/auth/Signup.vue` (2×), `src/pages/auth/CheckTheEmail.vue` (1×): `push({ name: "explore" })`.
|
- `src/pages/auth/Login.vue` (2×), `src/pages/auth/Signup.vue` (2×), `src/pages/auth/CheckTheEmail.vue` (1×): `push({ name: "explore" })`.
|
||||||
- `src/pages/workouts/Feed.vue`: изменений не потребовалось (ссылок на dashboard нет).
|
- `src/pages/workouts/Feed.vue`: изменений не потребовалось (ссылок на dashboard нет).
|
||||||
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite `✓ built in 6.99s`; pre-existing >500kB chunk warning).
|
- Verified: `yarn lint` exit 0, `yarn build` exit 0 (vue-tsc --noEmit 0 ошибок, vite `✓ built in 6.99s`; pre-existing >500kB chunk warning).
|
||||||
|
|
||||||
|
## TASK-HIDE-TRACK-T1 (2026-09-25)
|
||||||
|
|
||||||
|
- `../backend/tests/test_api_workout.py`: добавлен `test_workout_hide_start_end` (PATCH hide_start_end False→True→False + GET-верификация)
|
||||||
|
- Результат: `python -m pytest tests/test_api_workout.py -v` → **4 passed (5.13s)**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ integration stack (`yarn up`) so the SPA talks to the real backend at `:8000`.
|
||||||
## Reporting
|
## Reporting
|
||||||
|
|
||||||
When finishing a task, state explicitly in the completion message:
|
When finishing a task, state explicitly in the completion message:
|
||||||
|
|
||||||
- lint/build result
|
- lint/build result
|
||||||
- which browser verification was performed (MCP ad-hoc vs Playwright spec) and
|
- which browser verification was performed (MCP ad-hoc vs Playwright spec) and
|
||||||
the outcome (pass/fail)
|
the outcome (pass/fail)
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ export type WorkoutItem = {
|
||||||
latitude: number;
|
latitude: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
|
hide_start_end: boolean;
|
||||||
workouted_at: string;
|
workouted_at: string;
|
||||||
external_links?: WorkoutLink;
|
external_links?: WorkoutLink;
|
||||||
photos?: Array<WorkoutPhoto>;
|
photos?: Array<WorkoutPhoto>;
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { useToast } from "vuestic-ui/web-components";
|
||||||
import WorkoutItemComponent from "./components/WorkoutItem.vue";
|
import WorkoutItemComponent from "./components/WorkoutItem.vue";
|
||||||
import { WorkoutItem, ChartDataByMetric } from "./Definitions.vue";
|
import { WorkoutItem, ChartDataByMetric } from "./Definitions.vue";
|
||||||
import { GetWorkout, InitWorkoutItem } from "./components/GetWorkout";
|
import { GetWorkout, InitWorkoutItem } from "./components/GetWorkout";
|
||||||
|
import { trimTrack } from "./components/TrimTrack";
|
||||||
|
|
||||||
const { init } = useToast();
|
const { init } = useToast();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
@ -36,14 +37,31 @@ const dzenLink = ref("");
|
||||||
const initWorkout = (id: string) => {
|
const initWorkout = (id: string) => {
|
||||||
GetWorkout(`/api/v0/public/workouts/${id}`)
|
GetWorkout(`/api/v0/public/workouts/${id}`)
|
||||||
.then((d: InitWorkoutItem) => {
|
.then((d: InitWorkoutItem) => {
|
||||||
mapCenter.value = d.mapCenter;
|
|
||||||
lineCoordinates.value = d.lineCoordinates;
|
|
||||||
distances.value = d.distances;
|
|
||||||
workoutItem.value = d.workoutItem;
|
workoutItem.value = d.workoutItem;
|
||||||
data.value = d.data;
|
|
||||||
if (d.dzenLink != undefined) {
|
if (d.dzenLink != undefined) {
|
||||||
dzenLink.value = d.dzenLink;
|
dzenLink.value = d.dzenLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply start/end trimming for public view
|
||||||
|
if (d.workoutItem.hide_start_end) {
|
||||||
|
const totalDist =
|
||||||
|
d.distances.length > 0 ? d.distances[d.distances.length - 1] : 0;
|
||||||
|
const trimmed = trimTrack(
|
||||||
|
d.lineCoordinates,
|
||||||
|
d.distances,
|
||||||
|
d.data,
|
||||||
|
totalDist,
|
||||||
|
);
|
||||||
|
lineCoordinates.value = trimmed.lineCoordinates;
|
||||||
|
distances.value = trimmed.distances;
|
||||||
|
data.value = trimmed.data;
|
||||||
|
} else {
|
||||||
|
lineCoordinates.value = d.lineCoordinates;
|
||||||
|
distances.value = d.distances;
|
||||||
|
data.value = d.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapCenter.value = d.mapCenter;
|
||||||
})
|
})
|
||||||
.catch((_error: AxiosError) => {
|
.catch((_error: AxiosError) => {
|
||||||
init({
|
init({
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { ChartDataByMetric } from "../Definitions.vue";
|
||||||
|
|
||||||
|
export interface TrimResult {
|
||||||
|
lineCoordinates: Array<Array<number>>;
|
||||||
|
distances: Array<number>;
|
||||||
|
data: ChartDataByMetric;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Distance between two [lon, lat] coordinates in meters (sphere haversine). */
|
||||||
|
const haversine = (a: Array<number>, b: Array<number>): number => {
|
||||||
|
const R = 6371000;
|
||||||
|
const toRad = (deg: number): number => (deg * Math.PI) / 180;
|
||||||
|
const dLat = toRad(b[1] - a[1]);
|
||||||
|
const dLon = toRad(b[0] - a[0]);
|
||||||
|
const h =
|
||||||
|
Math.sin(dLat / 2) ** 2 +
|
||||||
|
Math.cos(toRad(a[1])) * Math.cos(toRad(b[1])) * Math.sin(dLon / 2) ** 2;
|
||||||
|
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trims GPS track from both ends by the given number of meters.
|
||||||
|
* Formula: trim = min(500, totalDistance * 0.05)
|
||||||
|
* Returns the trimmed data ready for display.
|
||||||
|
* If totalDistance < 2 * trimMeters, returns original data unchanged.
|
||||||
|
*/
|
||||||
|
export function trimTrack(
|
||||||
|
lineCoordinates: Array<Array<number>>,
|
||||||
|
distances: Array<number>,
|
||||||
|
data: ChartDataByMetric,
|
||||||
|
totalDistanceMeters: number,
|
||||||
|
): TrimResult {
|
||||||
|
const trimMeters = Math.min(500, totalDistanceMeters * 0.05);
|
||||||
|
|
||||||
|
// If track too short to trim, return original data unchanged
|
||||||
|
if (totalDistanceMeters < trimMeters * 2) {
|
||||||
|
return {
|
||||||
|
lineCoordinates,
|
||||||
|
distances,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find startIdx: first index where distances[startIdx] >= trimMeters
|
||||||
|
let startIdx = 0;
|
||||||
|
for (let i = 0; i < distances.length; i++) {
|
||||||
|
if (distances[i] >= trimMeters) {
|
||||||
|
startIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find endIdx: last index where totalDistance - distances[endIdx] >= trimMeters
|
||||||
|
let endIdx = distances.length - 1;
|
||||||
|
for (let i = distances.length - 1; i >= 0; i--) {
|
||||||
|
if (totalDistanceMeters - distances[i] >= trimMeters) {
|
||||||
|
endIdx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build trimmed coordinates with interpolation at boundaries
|
||||||
|
const trimmedCoords: Array<Array<number>> = [];
|
||||||
|
|
||||||
|
// Start interpolation: if distances[startIdx] > trimMeters, interpolate between startIdx-1 and startIdx
|
||||||
|
if (startIdx > 0 && distances[startIdx] > trimMeters) {
|
||||||
|
const t =
|
||||||
|
(trimMeters - distances[startIdx - 1]) /
|
||||||
|
(distances[startIdx] - distances[startIdx - 1]);
|
||||||
|
const newLon =
|
||||||
|
lineCoordinates[startIdx - 1][0] +
|
||||||
|
t * (lineCoordinates[startIdx][0] - lineCoordinates[startIdx - 1][0]);
|
||||||
|
const newLat =
|
||||||
|
lineCoordinates[startIdx - 1][1] +
|
||||||
|
t * (lineCoordinates[startIdx][1] - lineCoordinates[startIdx - 1][1]);
|
||||||
|
trimmedCoords.push([newLon, newLat]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middle points: from startIdx to endIdx inclusive
|
||||||
|
for (let i = startIdx; i <= endIdx; i++) {
|
||||||
|
trimmedCoords.push([lineCoordinates[i][0], lineCoordinates[i][1]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// End interpolation: if totalDistance - distances[endIdx] > trimMeters, interpolate between endIdx and endIdx+1
|
||||||
|
if (
|
||||||
|
endIdx < distances.length - 1 &&
|
||||||
|
totalDistanceMeters - distances[endIdx] > trimMeters
|
||||||
|
) {
|
||||||
|
const t =
|
||||||
|
(totalDistanceMeters - trimMeters - distances[endIdx]) /
|
||||||
|
(distances[endIdx + 1] - distances[endIdx]);
|
||||||
|
const newLon =
|
||||||
|
lineCoordinates[endIdx][0] +
|
||||||
|
t * (lineCoordinates[endIdx + 1][0] - lineCoordinates[endIdx][0]);
|
||||||
|
const newLat =
|
||||||
|
lineCoordinates[endIdx][1] +
|
||||||
|
t * (lineCoordinates[endIdx + 1][1] - lineCoordinates[endIdx][1]);
|
||||||
|
trimmedCoords.push([newLon, newLat]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recalculate distances cumulatively from 0 for the trimmed track
|
||||||
|
const newDistances: Array<number> = [0];
|
||||||
|
for (let i = 1; i < trimmedCoords.length; i++) {
|
||||||
|
newDistances.push(
|
||||||
|
newDistances[i - 1] + haversine(trimmedCoords[i - 1], trimmedCoords[i]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim chart data using the same index range [startIdx, endIdx]
|
||||||
|
const trimmedData: ChartDataByMetric = {};
|
||||||
|
const keys = Object.keys(data) as Array<keyof ChartDataByMetric>;
|
||||||
|
for (const key of keys) {
|
||||||
|
const metric = data[key];
|
||||||
|
if (metric) {
|
||||||
|
trimmedData[key] = {
|
||||||
|
labels: metric.labels.slice(startIdx, endIdx + 1),
|
||||||
|
datasets: metric.datasets.map((ds) => ({
|
||||||
|
...ds,
|
||||||
|
data: ds.data.slice(startIdx, endIdx + 1),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
lineCoordinates: trimmedCoords,
|
||||||
|
distances: newDistances,
|
||||||
|
data: trimmedData,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue