Hull-diagram v2: rettelser fra ekte produksjonsbruk (ADR-084 del 2)
Ett-ikon-overlapp-fiks i midtbanen (green-ikonet manglet reservert plass), bak-green-rendering (hindringer bak selve greenen vises nå over green-ikonet i stedet for klemt inn i vanlig-sonen), fem nye hindringsikoner, og en omskrevet liste (én rad per hindring, med bane/posisjon-info). Setter i tillegg ekte hazard_group-verdier på Tjømes koordinatdata, utledet fra klubbens eget koordinat-regneark (som viste seg å ha en egen gruppenummer-kolonne) i stedet for en heuristikk -- løser definitivt de flerbunker-hullene forrige runde bevisst lot stå uklassifisert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
1bfac02944
commit
f3b3d30e4e
10 changed files with 324 additions and 37 deletions
|
|
@ -8453,6 +8453,99 @@ til bruker (i dag kun `rock`/"Fjellknaus"). V0-prompt for polert
|
|||
visning skrives etter at bruker har godkjent den hånd-kodede
|
||||
versjonen.
|
||||
|
||||
### ADR-084 del 2: retting fra ekte produksjonsbruk, ny hazard_group-data for Tjøme (2026-08-18)
|
||||
|
||||
Bruker viste et ekte skjermbilde av v2-diagrammet i produksjon (samme
|
||||
dag som utrullingen over) og meldte fem konkrete avvik, pluss lastet
|
||||
opp fem nye 32px-ikoner (fjellknaus/trær/dogleg/layup/landemerke) og et
|
||||
regneark med Tjømes koordinater. Alle fem rettet:
|
||||
|
||||
1. **Ett ikon per hindring, ikke to** -- viste seg IKKE å være en kode-
|
||||
feil, men at ekte Tjøme-data manglet `hazard_group` (forventet, se
|
||||
"gjenstår" over). Løst ved å faktisk sette `hazard_group` på ekte
|
||||
data (se under).
|
||||
2. **Ikon-overlapp med green** -- reell kode-feil: `layoutLane()` sin
|
||||
kollisjons-kaskade for midtbanen tok ikke hensyn til at green-ikonet
|
||||
selv opptar plass i samme sone. Fikset med `CENTER_TRACK_TOP`
|
||||
(reservert sone under green-ikonet, kun for midtbanen).
|
||||
3. **Skal ikonene i listen under diagrammet gjenbrukes, evt. i større
|
||||
størrelse?** Bruker spurte oss om å vurdere -- svarte ja, 36px (opp
|
||||
fra det gamle universelle 20px `TriangleAlert`-ikonet), forskjellig
|
||||
ikon per type.
|
||||
4. **Listen manglet forkant/senter/bakkant og venstre/senter/høyre-info**
|
||||
-- lagt til som en undertekst-linje per rad.
|
||||
5. **Listen viste hvert par to ganger** -- skrevet om til én rad per
|
||||
hindring, med "Forkant X m / Bakkant Y m" på samme rad når parret.
|
||||
|
||||
**Under samme runde, ny rettelse fra bruker etter at "bakre
|
||||
greenbunker" (hull 4) skulle pares:** "(Den må jo for øvrig vises OVER
|
||||
greenikonet?)" -- riktig observasjon: `alongPercent` (fra
|
||||
`projectOntoAxis`, se hole-geometry.ts) klippes bevisst IKKE til
|
||||
[0,100] i geometrimodulen, men visningen klippet den likevel ved selve
|
||||
tegningen, så en hindring bak selve greenen (alongPercent > 100) endte
|
||||
opp i SAMME sone som "helt inntil green"-hindringer -- fysisk feil
|
||||
side av green-ikonet. Fikset i `hole-diagram-view.tsx`: når minst én
|
||||
midtbane-hindring har alongPercent > 100, flyttes green-ikonet lenger
|
||||
ned (`GREEN_TOP_PCT_PUSHED`), og en egen liten sone åpnes OVER det
|
||||
flyttede ikonet (`BEHIND_GREEN_ZONE_TOP/BOTTOM`), med kollisjons-
|
||||
kaskade i motsatt retning (`layoutBehindGreenLane`, kaskaderer OPPOVER
|
||||
i stedet for nedover). Ingen tilsvarende sone bak TEE ennå -- ingen
|
||||
kjent forekomst i data, men samme mønster kan gjenbrukes symmetrisk.
|
||||
|
||||
**Ny `hazard_group`-data for Tjøme, fra ekte kildedata (ikke
|
||||
heuristikk):** brukeren lastet opp klubbens eget regneark med
|
||||
koordinater, som viste seg å ha en EKSPLISITT gruppenummer-kolonne som
|
||||
parer forkant/bakkant til samme fysiske hindring -- akkurat den
|
||||
identiteten `hazard_group` (del 1, migrasjon 080) var designet for å
|
||||
representere. Kryssjekket regnearkets 38 gruppenumre mot databasens
|
||||
180 koordinatpunkter via lat/long-matching (Python-script, engangs,
|
||||
scratchpad): 76 av 76 gyldige punkter identifisert eksakt. Løste
|
||||
DEFINITIVT de to tilfellene forrige runde bevisst utelot som
|
||||
tvetydige (hull 4: tre høyre fairway-bunkere, hull 13: to) ved
|
||||
eliminasjon -- 2 av regnearkets rader hadde feil koordinatformat (UTM/
|
||||
EPSG:25833 i stedet for WGS84, sannsynligvis kopi-lim-feil ved
|
||||
regnearkets forfatning), men gruppenummeret var likevel nok til å
|
||||
identifisere DB-raden ved eliminasjon (de gjenværende umatchede
|
||||
punktene på riktig hull+side+type). Fant i tillegg at `rock`
|
||||
("fjellknaus") også har forkant/bakkant-par på fire hull -- ikke
|
||||
fanget opp i del 1 siden rock da bare hadde generisk ikon. Bruker
|
||||
bekreftet i tillegg manuelt én ekstra paring regnearket selv ikke
|
||||
hadde gruppenummer for (hull 4 sin "bakre greenbunker").
|
||||
|
||||
**Resultat: 78 koordinatrader satt til 39 distinkte `hazard_group`-
|
||||
verdier** (format `{hull}-{regneark-gruppenummer}`, sporbart tilbake
|
||||
til kilden), kjørt som en frittstående `UPDATE ... FROM (VALUES ...)`
|
||||
mot ekte `teecup_db` (ikke en migrasjon -- datainnhold, ikke skjema).
|
||||
|
||||
**Bevisst IKKE gjort:** bekk (creek) fikk et eget ikon fra bruker
|
||||
(`hazard-creek.png`, kopiert til `frontend/public/hole-diagram/`, IKKE
|
||||
koblet til noe ennå) -- domenet `course_poi_type` har ingen egen verdi
|
||||
for bekk i dag (kun `water` dekker både dam og bekk), så å skille dem
|
||||
visuelt krever en ny domeneverdi + reklassifisering av de spesifikke
|
||||
bekk-punktene regnearket identifiserer. Bruker ba eksplisitt om at
|
||||
dette gjøres som en EGEN runde/beslutning, ikke bakt inn i denne.
|
||||
|
||||
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55 (to ganger,
|
||||
etter hvert av de to kode-rundene). Ny scratch-runde (database+API+
|
||||
next dev, samme disiplin) med presist konstruert "bak green"-scenario
|
||||
-- bekreftet visuelt at bakre greenbunker faktisk rendres OVER
|
||||
green-ikonet, ikke i normal-sonen, med normal-sonens egne hindringer
|
||||
uendret ved siden av. Lys+mørk bekreftet begge runder. Begge scratch-
|
||||
stackene fullstendig revet ned.
|
||||
|
||||
**Rullet ut 2026-08-18** -- bruker bekreftet begge deler eksplisitt
|
||||
(kode + data). `docker compose build teecup_frontend && up -d`
|
||||
(ingen migrasjon -- kun frontend-kode denne runden), deretter
|
||||
`UPDATE`-en over kjørt mot ekte `teecup_db`. Rene containerlogger,
|
||||
`https://teecup.golf/logg-inn` 200 OK, 78/78 rader bekreftet satt
|
||||
(`count(*) ... WHERE hazard_group IS NOT NULL` = 78, 39 distinkte).
|
||||
|
||||
**Gjenstår:** bekk (creek) som egen `poi_type` -- egen runde, bruker
|
||||
har bedt om dette eksplisitt. Liste over `_HAZARD_CONFIG`-typer uten
|
||||
eget ikon oppdatert: nå KUN `road` (ingen ikon lastet opp for denne).
|
||||
V0-prompt for polert visning fortsatt ikke skrevet -- venter på at
|
||||
bruker har sett/godkjent denne runden i produksjon også.
|
||||
|
||||
---
|
||||
|
||||
## Utviklingsplan (rekkefølge)
|
||||
|
|
|
|||
70
CHANGELOG.md
70
CHANGELOG.md
|
|
@ -12110,3 +12110,73 @@ Neste steg:
|
|||
(i dag kun `rock`/"Fjellknaus") leveres til bruker. V0-prompt for
|
||||
polert visning skrives etter godkjenning av den hånd-kodede
|
||||
versjonen.
|
||||
|
||||
102. **Hull-diagram v2, retting fra ekte produksjonsbruk (ADR-084 del
|
||||
2) — 2026-08-18.** Bruker viste et ekte skjermbilde av v2-
|
||||
diagrammet i produksjon og meldte fem avvik: (1) hindringer med
|
||||
forkant+bakkant viste to ikoner i stedet for ett -- skyldtes at
|
||||
ekte Tjøme-data ennå ikke hadde `hazard_group` satt, ikke en
|
||||
kode-feil. (2) Et ikon overlappet green-ikonet i midtbanen -- ekte
|
||||
kode-feil, `layoutLane()` tok ikke hensyn til green-ikonets egen
|
||||
plass; fikset med en reservert sone (`CENTER_TRACK_TOP`). (3-4)
|
||||
Listen under diagrammet manglet forkant/senter/bakkant- og
|
||||
venstre/senter/høyre-info, og viste hvert par to ganger -- skrevet
|
||||
om til én rad per hindring med begge avstander på samme rad, pluss
|
||||
undertekst for bane/posisjon. (5) Ikonene i listen byttet fra
|
||||
universelt 20px `TriangleAlert` til type-spesifikke ikoner ved
|
||||
36px.
|
||||
|
||||
Underveis, da hull 4 sin "bakre greenbunker" skulle pares: bruker
|
||||
påpekte at den fysisk ligger BAK selve greenen og derfor må vises
|
||||
OVER green-ikonet, ikke i vanlig-sonen. `alongPercent` fra
|
||||
`projectOntoAxis` klippes bevisst ikke i geometrimodulen (kan bli
|
||||
>100), men visningen klippet den likevel -- fikset ved å flytte
|
||||
green-ikonet ned og åpne en egen sone over det
|
||||
(`GREEN_TOP_PCT_PUSHED`/`BEHIND_GREEN_ZONE_*`,
|
||||
`layoutBehindGreenLane` kaskaderer oppover) når minst én
|
||||
midtbane-hindring faktisk ligger bak green.
|
||||
|
||||
Fem nye ikoner (fjellknaus/trær/dogleg/layup/landemerke, alle
|
||||
32px) integrert -- disse typene var i domenet fra før men manglet
|
||||
egen visning i diagrammet (fjellknaus hadde generisk ikon, resten
|
||||
var usynlige der).
|
||||
|
||||
**Ny ekte `hazard_group`-data for Tjøme, fra kildedata, ikke
|
||||
heuristikk:** bruker lastet opp klubbens eget koordinat-regneark,
|
||||
som viste seg å ha en EKSPLISITT gruppenummer-kolonne som allerede
|
||||
parer forkant/bakkant -- akkurat identiteten `hazard_group` (del 1)
|
||||
var designet for. Kryssjekket regnearkets 38 grupper mot databasens
|
||||
180 punkter via lat/long (engangs Python-script) -- løste
|
||||
DEFINITIVT de to sakene forrige runde bevisst lot stå
|
||||
(hull 4: tre høyre fairway-bunkere, hull 13: to) ved eliminasjon,
|
||||
til tross for at 2 rader i regnearket hadde feil koordinatformat
|
||||
(UTM/EPSG:25833 -- trolig kopi-lim-feil, uskadelig siden kun
|
||||
gruppenummeret var nødvendig). Fant i tillegg at `rock` også har
|
||||
forkant/bakkant-par på fire hull (ikke fanget opp i del 1). Bruker
|
||||
bekreftet manuelt én ekstra paring uten gruppenummer i regnearket
|
||||
(hull 4 sin bakre greenbunker). Resultat: 78 rader satt til 39
|
||||
distinkte `hazard_group`-verdier (`{hull}-{regneark-gruppe}`,
|
||||
sporbart til kilden) via en frittstående `UPDATE ... FROM
|
||||
(VALUES ...)` -- datainnhold, ikke skjema, ingen ny migrasjon.
|
||||
|
||||
**Bevisst utsatt:** bruker lastet opp et bekk (creek)-ikon, men
|
||||
domenet `course_poi_type` skiller ikke bekk fra dam (begge
|
||||
`water`) -- å vise dem ulikt krever en ny domeneverdi +
|
||||
reklassifisering av spesifikke punkter. Bruker ba eksplisitt om at
|
||||
dette blir en EGEN runde, ikke bakt inn her.
|
||||
|
||||
**Verifisert:** `tsc --noEmit` rent, `vitest run` 55/55 (to ganger).
|
||||
Ny scratch-runde med presist konstruert "bak green"-scenario --
|
||||
bekreftet visuelt at bakre greenbunker rendres over green-ikonet,
|
||||
normal-sonens hindringer uendret ved siden av. Lys+mørk bekreftet
|
||||
begge runder. Begge scratch-stackene revet ned.
|
||||
|
||||
**Rullet ut 2026-08-18** -- bruker bekreftet kode og data hver for
|
||||
seg. `docker compose build teecup_frontend && up -d` (kun
|
||||
frontend-kode, ingen migrasjon), deretter UPDATE-en mot ekte
|
||||
`teecup_db`. Rene containerlogger, `https://teecup.golf/logg-inn`
|
||||
200 OK, 78/78 rader bekreftet satt (39 distinkte grupper).
|
||||
|
||||
**Gjenstår:** bekk som egen `poi_type` (egen runde, bedt om
|
||||
eksplisitt av bruker). Ikon-mangel-lista er nå kun `road`. V0-
|
||||
prompt for polert visning fortsatt ikke skrevet.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
import { MapPin, TriangleAlert } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type HazardKind = "sand" | "water" | "generic"
|
||||
export type HazardKind = "sand" | "water" | "rock" | "tree" | "dogleg" | "layup" | "landmark" | "generic"
|
||||
|
||||
export type DiagramHazard = {
|
||||
key: string
|
||||
|
|
@ -27,6 +27,21 @@ export type DiagramHazard = {
|
|||
* hindringen er et ekte forkant+bakkant-par (se hazard_group i
|
||||
* datalaget). `null` = enkeltpunkt, ingen tekst over ikonet. */
|
||||
above: number | null
|
||||
/** Kun meningsfullt når `above` er `null` -- brukes i listen under
|
||||
* diagrammet til å si "forkant"/"senter"/"bakkant" om et
|
||||
* enkeltstående punkt. */
|
||||
location: "front" | "middle" | "back" | null
|
||||
}
|
||||
|
||||
const _LANE_LABEL: Record<DiagramHazard["lane"], string> = {
|
||||
left: "Venstre",
|
||||
center: "Senter",
|
||||
right: "Høyre",
|
||||
}
|
||||
const _LOCATION_LABEL: Record<"front" | "middle" | "back", string> = {
|
||||
front: "forkant",
|
||||
middle: "midtpunkt",
|
||||
back: "bakkant",
|
||||
}
|
||||
|
||||
type Props = {
|
||||
|
|
@ -39,40 +54,78 @@ type Props = {
|
|||
|
||||
const TRACK_TOP = 12 // where alongPercent === 100 lands (leaves room for the flag)
|
||||
const TRACK_BOTTOM = 88 // where alongPercent === 0 lands (leaves room for the tee dot)
|
||||
// Midtbanen har i tillegg faste green-/tee-ankre tegnet OVENPÅ selve
|
||||
// sporet (green-ikon+etikett øverst, tee-prikk+etikett nederst) -- en
|
||||
// hindring plassert etter samme skala som venstre/høyre-banen kunne
|
||||
// dermed havne visuelt oppå green-ikonet. Reservert sone er banens
|
||||
// egen skala, kun for midtbanen.
|
||||
const CENTER_TRACK_TOP = 29
|
||||
const CENTER_TRACK_BOTTOM = 80
|
||||
const LABEL_MIN_GAP_PCT = 16 // hindringer i samme bane trenger mer luft nå (ikon + to tall)
|
||||
|
||||
// En hindring KAN ligge bak selve greenen (alongPercent > 100 -- f.eks.
|
||||
// en bakre greenbunker) -- projectOntoAxis() klipper bevisst IKKE dette
|
||||
// bort (se hole-geometry.ts). Uten særbehandling ville en slik hindring
|
||||
// blitt klippet til samme posisjon som "helt inntil green" og dermed
|
||||
// visuelt smeltet sammen med front-of-green-hindringer, selv om den
|
||||
// fysisk ligger på MOTSATT side av green-ikonet. Løsning: når minst én
|
||||
// senterbane-hindring er bak green, flyttes green-ankeret lenger ned
|
||||
// (fra faste 3% til GREEN_TOP_PCT_PUSHED), og en egen, liten sone
|
||||
// ovenfor det flyttede ankeret reserveres til disse hindringene --
|
||||
// nærmest green kaskaderer OPPOVER (motsatt retning av layoutLane) ved
|
||||
// kollisjon. Ingen tilsvarende sone finnes (ennå) for hindringer bak
|
||||
// TEE -- ingen kjent forekomst i dagens data, men samme mønster kan
|
||||
// gjenbrukes symmetrisk om det trengs.
|
||||
const GREEN_TOP_PCT_DEFAULT = 3
|
||||
const GREEN_TOP_PCT_PUSHED = 22
|
||||
const CENTER_TRACK_TOP_PUSHED = 40
|
||||
const BEHIND_GREEN_ZONE_TOP = 3
|
||||
const BEHIND_GREEN_ZONE_BOTTOM = 19
|
||||
const BEHIND_GREEN_MAX_OVERSHOOT = 40 // % av hull-lengden bak green før visuelt klippet til toppen
|
||||
|
||||
function clamp(n: number, lo: number, hi: number) {
|
||||
return Math.min(hi, Math.max(lo, n))
|
||||
}
|
||||
|
||||
function alongToTopPct(alongPercent: number) {
|
||||
function alongToTopPct(alongPercent: number, top = TRACK_TOP, bottom = TRACK_BOTTOM) {
|
||||
const a = clamp(alongPercent, 0, 100)
|
||||
return TRACK_TOP + ((100 - a) / 100) * (TRACK_BOTTOM - TRACK_TOP)
|
||||
return top + ((100 - a) / 100) * (bottom - top)
|
||||
}
|
||||
|
||||
function formatMeters(m: number) {
|
||||
return `${Math.round(m)} m`
|
||||
}
|
||||
|
||||
function HazardIcon({ kind }: { kind: HazardKind }) {
|
||||
if (kind === "sand") {
|
||||
return <img src="/hole-diagram/hazard-sand.png" width={32} height={31} alt="" className="block" />
|
||||
}
|
||||
if (kind === "water") {
|
||||
return <img src="/hole-diagram/hazard-water.png" width={32} height={31} alt="" className="block" />
|
||||
const _ICON_SRC: Partial<Record<HazardKind, string>> = {
|
||||
sand: "/hole-diagram/hazard-sand.png",
|
||||
water: "/hole-diagram/hazard-water.png",
|
||||
rock: "/hole-diagram/hazard-rock.png",
|
||||
tree: "/hole-diagram/hazard-tree.png",
|
||||
dogleg: "/hole-diagram/hazard-dogleg.png",
|
||||
layup: "/hole-diagram/hazard-layup.png",
|
||||
landmark: "/hole-diagram/hazard-landmark.png",
|
||||
}
|
||||
|
||||
function HazardIcon({ kind, size = 32 }: { kind: HazardKind; size?: number }) {
|
||||
const src = _ICON_SRC[kind]
|
||||
if (src) {
|
||||
return <img src={src} width={size} height={size} alt="" className="block" style={{ height: "auto" }} />
|
||||
}
|
||||
return <TriangleAlert className="size-7 text-cup-strong" strokeWidth={2.5} />
|
||||
}
|
||||
|
||||
/** Nudger ikoner i SAMME bane fra øverst til nederst hvis de er for
|
||||
* nære vertikalt -- samme kollisjons-mønster som før, men kjørt per
|
||||
* bane siden hindringer nå er spredt over tre baner. */
|
||||
function layoutLane(hazards: DiagramHazard[]) {
|
||||
const sorted = [...hazards].sort((a, b) => alongToTopPct(a.alongPercent) - alongToTopPct(b.alongPercent))
|
||||
* bane siden hindringer nå er spredt over tre baner. `top`/`bottom`
|
||||
* lar midtbanen reservere plass til de faste green-/tee-ankrene. */
|
||||
function layoutLane(hazards: DiagramHazard[], top = TRACK_TOP, bottom = TRACK_BOTTOM) {
|
||||
const sorted = [...hazards].sort(
|
||||
(a, b) => alongToTopPct(a.alongPercent, top, bottom) - alongToTopPct(b.alongPercent, top, bottom),
|
||||
)
|
||||
const placed: (DiagramHazard & { topPct: number })[] = []
|
||||
let lastTop = Number.NEGATIVE_INFINITY
|
||||
for (const h of sorted) {
|
||||
const desired = alongToTopPct(h.alongPercent)
|
||||
const desired = alongToTopPct(h.alongPercent, top, bottom)
|
||||
const topPct = Math.max(desired, lastTop + LABEL_MIN_GAP_PCT)
|
||||
placed.push({ ...h, topPct })
|
||||
lastTop = topPct
|
||||
|
|
@ -80,6 +133,34 @@ function layoutLane(hazards: DiagramHazard[]) {
|
|||
return placed
|
||||
}
|
||||
|
||||
function behindGreenDesiredTopPct(alongPercent: number) {
|
||||
const overshoot = clamp(alongPercent - 100, 0, BEHIND_GREEN_MAX_OVERSHOOT)
|
||||
// 0 overshoot (rett bak green) -> bunnen av sonen (nærmest green-ikonet).
|
||||
// Maks overshoot -> toppen av sonen (lengst unna, øverst i diagrammet).
|
||||
return (
|
||||
BEHIND_GREEN_ZONE_BOTTOM -
|
||||
(overshoot / BEHIND_GREEN_MAX_OVERSHOOT) * (BEHIND_GREEN_ZONE_BOTTOM - BEHIND_GREEN_ZONE_TOP)
|
||||
)
|
||||
}
|
||||
|
||||
/** Speilvendt av layoutLane() -- kaskaderer OPPOVER (mot diagrammets
|
||||
* toppkant) ved kollisjon, siden disse hindringene ligger bak green,
|
||||
* ikke foran den. */
|
||||
function layoutBehindGreenLane(hazards: DiagramHazard[]) {
|
||||
const sorted = [...hazards].sort(
|
||||
(a, b) => behindGreenDesiredTopPct(b.alongPercent) - behindGreenDesiredTopPct(a.alongPercent),
|
||||
)
|
||||
const placed: (DiagramHazard & { topPct: number })[] = []
|
||||
let lastTop = Number.POSITIVE_INFINITY
|
||||
for (const h of sorted) {
|
||||
const desired = behindGreenDesiredTopPct(h.alongPercent)
|
||||
const topPct = Math.min(desired, lastTop - LABEL_MIN_GAP_PCT)
|
||||
placed.push({ ...h, topPct })
|
||||
lastTop = topPct
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
function HazardMarker({ hazard, topPct }: { hazard: DiagramHazard; topPct: number }) {
|
||||
return (
|
||||
<div
|
||||
|
|
@ -132,7 +213,14 @@ export function HoleDiagram({ distances, playerPosition, hazards, size = "full",
|
|||
}
|
||||
|
||||
const leftHazards = layoutLane(hazards.filter((h) => h.lane === "left"))
|
||||
const centerHazards = layoutLane(hazards.filter((h) => h.lane === "center"))
|
||||
const centerAll = hazards.filter((h) => h.lane === "center")
|
||||
const centerBehindGreen = centerAll.filter((h) => h.alongPercent > 100)
|
||||
const centerNormal = centerAll.filter((h) => h.alongPercent <= 100)
|
||||
const hasBehindGreen = centerBehindGreen.length > 0
|
||||
const greenTopPct = hasBehindGreen ? GREEN_TOP_PCT_PUSHED : GREEN_TOP_PCT_DEFAULT
|
||||
const centerTrackTop = hasBehindGreen ? CENTER_TRACK_TOP_PUSHED : CENTER_TRACK_TOP
|
||||
const centerHazards = layoutLane(centerNormal, centerTrackTop, CENTER_TRACK_BOTTOM)
|
||||
const behindGreenHazards = layoutBehindGreenLane(centerBehindGreen)
|
||||
const rightHazards = layoutLane(hazards.filter((h) => h.lane === "right"))
|
||||
const playerTop = alongToTopPct(playerPosition.alongPercent)
|
||||
|
||||
|
|
@ -160,7 +248,8 @@ export function HoleDiagram({ distances, playerPosition, hazards, size = "full",
|
|||
<div className="relative border-x border-clubhouse-border/60">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute left-1/2 top-[3%] flex -translate-x-1/2 flex-col items-center gap-0.5"
|
||||
className="absolute left-1/2 flex -translate-x-1/2 flex-col items-center gap-0.5"
|
||||
style={{ top: `${greenTopPct}%` }}
|
||||
>
|
||||
<img src="/hole-diagram/green-40.png" width={40} height={40} alt="" className="block" />
|
||||
<span className="rounded bg-clubhouse-card px-1.5 py-0.5 text-xs font-bold text-tee-strong">
|
||||
|
|
@ -168,6 +257,10 @@ export function HoleDiagram({ distances, playerPosition, hazards, size = "full",
|
|||
</span>
|
||||
</div>
|
||||
|
||||
{behindGreenHazards.map((h) => (
|
||||
<HazardMarker key={h.key} hazard={h} topPct={h.topPct} />
|
||||
))}
|
||||
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="absolute bottom-[3%] left-1/2 flex -translate-x-1/2 flex-col items-center gap-0.5"
|
||||
|
|
@ -208,14 +301,9 @@ export function HoleDiagram({ distances, playerPosition, hazards, size = "full",
|
|||
Hindringer
|
||||
</h3>
|
||||
<ul className="flex flex-col divide-y divide-clubhouse-border rounded-xl border border-clubhouse-border bg-clubhouse-card">
|
||||
{hazards.flatMap((h) =>
|
||||
h.above !== null
|
||||
? [
|
||||
<HazardRow key={`${h.key}-front`} label={`${h.label} (front)`} value={h.below} />,
|
||||
<HazardRow key={`${h.key}-back`} label={`${h.label} (bak)`} value={h.above} />,
|
||||
]
|
||||
: [<HazardRow key={h.key} label={h.label} value={h.below} />],
|
||||
)}
|
||||
{hazards.map((h) => (
|
||||
<HazardRow key={h.key} hazard={h} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -223,12 +311,30 @@ export function HoleDiagram({ distances, playerPosition, hazards, size = "full",
|
|||
)
|
||||
}
|
||||
|
||||
function HazardRow({ label, value }: { label: string; value: number }) {
|
||||
function HazardRow({ hazard }: { hazard: DiagramHazard }) {
|
||||
const meta = [
|
||||
_LANE_LABEL[hazard.lane],
|
||||
hazard.above === null && hazard.location ? _LOCATION_LABEL[hazard.location] : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")
|
||||
return (
|
||||
<li className="flex items-center gap-3 px-3 py-2.5">
|
||||
<TriangleAlert aria-hidden="true" className="size-5 shrink-0 text-cup-strong" strokeWidth={2.5} />
|
||||
<span className="min-w-0 flex-1 text-pretty text-base text-clubhouse-ink">{label}</span>
|
||||
<span className="shrink-0 text-lg font-bold tabular-nums text-clubhouse-ink">{formatMeters(value)}</span>
|
||||
<HazardIcon kind={hazard.kind} size={36} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-pretty text-base text-clubhouse-ink">{hazard.label}</div>
|
||||
<div className="text-sm text-clubhouse-muted">{meta}</div>
|
||||
</div>
|
||||
{hazard.above !== null ? (
|
||||
<div className="flex shrink-0 flex-col items-end text-clubhouse-ink">
|
||||
<span className="text-sm font-bold tabular-nums">Forkant {formatMeters(hazard.below)}</span>
|
||||
<span className="text-sm font-bold tabular-nums">Bakkant {formatMeters(hazard.above)}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="shrink-0 text-lg font-bold tabular-nums text-clubhouse-ink">
|
||||
{formatMeters(hazard.below)}
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
|
@ -281,11 +387,14 @@ function buildDiagramAria(
|
|||
const base = `Baneskisse. Senter ${Math.round(distances.middle)} meter til green. Din posisjon markert.`
|
||||
if (hazards.length === 0) return `${base} Ingen hindringer.`
|
||||
const list = hazards
|
||||
.map((h) =>
|
||||
h.above !== null
|
||||
? `${h.label} forkant ${Math.round(h.below)} meter, bakkant ${Math.round(h.above)} meter`
|
||||
: `${h.label} ${Math.round(h.below)} meter`,
|
||||
)
|
||||
.join(", ")
|
||||
.map((h) => {
|
||||
const lane = _LANE_LABEL[h.lane].toLowerCase()
|
||||
if (h.above !== null) {
|
||||
return `${h.label}, ${lane}, forkant ${Math.round(h.below)} meter, bakkant ${Math.round(h.above)} meter`
|
||||
}
|
||||
const location = h.location ? `, ${_LOCATION_LABEL[h.location]}` : ""
|
||||
return `${h.label}, ${lane}${location}, ${Math.round(h.below)} meter`
|
||||
})
|
||||
.join("; ")
|
||||
return `${base} Hindringer: ${list}.`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,17 +54,26 @@ const _HAZARD_LABELS: Record<string, string> = {
|
|||
fairway_bunker: "Bunker (fairway)",
|
||||
water: "Vannhinder",
|
||||
rock: "Fjellknaus",
|
||||
trees: "Trær",
|
||||
dogleg: "Dogleg",
|
||||
layup: "Layup-punkt",
|
||||
landmark: "Landemerke",
|
||||
}
|
||||
|
||||
// ADR-084: hvilket ikon en hindringstype får i diagrammet. "generic"
|
||||
// (typer uten dedikert ikon, i dag kun rock) faller tilbake til
|
||||
// lucide sin TriangleAlert -- se hole-diagram-view.tsx.
|
||||
type HazardKind = "sand" | "water" | "generic"
|
||||
// (typer uten dedikert ikon -- i dag ingen, `road` ville falt hit hvis
|
||||
// den noen gang klassifiseres som hindring) faller tilbake til lucide
|
||||
// sin TriangleAlert -- se hole-diagram-view.tsx.
|
||||
type HazardKind = "sand" | "water" | "rock" | "tree" | "dogleg" | "layup" | "landmark" | "generic"
|
||||
const _HAZARD_KIND: Record<string, HazardKind> = {
|
||||
green_bunker: "sand",
|
||||
fairway_bunker: "sand",
|
||||
water: "water",
|
||||
rock: "generic",
|
||||
rock: "rock",
|
||||
trees: "tree",
|
||||
dogleg: "dogleg",
|
||||
layup: "layup",
|
||||
landmark: "landmark",
|
||||
}
|
||||
|
||||
// Grupperer rå hindringspunkter til hindrings-ENTITETER for diagrammet.
|
||||
|
|
@ -99,6 +108,10 @@ function groupDiagramHazards(
|
|||
alongPercent: number
|
||||
below: number
|
||||
above: number | null
|
||||
// Kun meningsfullt for enkeltstående punkt (above === null) --
|
||||
// et parret forkant+bakkant-hinder (above !== null) trenger ikke
|
||||
// dette, UI-en viser da alltid eksplisitt "forkant"/"bakkant".
|
||||
location: "front" | "middle" | "back" | null
|
||||
}
|
||||
const entities: Entity[] = []
|
||||
|
||||
|
|
@ -124,6 +137,7 @@ function groupDiagramHazards(
|
|||
alongPercent: alongValues.reduce((a, b) => a + b, 0) / alongValues.length,
|
||||
below: front ? distanceTo(front) : distanceTo(primary),
|
||||
above: front && back ? distanceTo(back) : null,
|
||||
location: front && back ? null : primary.location,
|
||||
})
|
||||
}
|
||||
for (const p of standalone) {
|
||||
|
|
@ -137,6 +151,7 @@ function groupDiagramHazards(
|
|||
alongPercent: along(p),
|
||||
below: distanceTo(p),
|
||||
above: null,
|
||||
location: p.location,
|
||||
})
|
||||
}
|
||||
return entities
|
||||
|
|
|
|||
BIN
frontend/public/hole-diagram/hazard-creek.png
Normal file
BIN
frontend/public/hole-diagram/hazard-creek.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
frontend/public/hole-diagram/hazard-dogleg.png
Normal file
BIN
frontend/public/hole-diagram/hazard-dogleg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/public/hole-diagram/hazard-landmark.png
Normal file
BIN
frontend/public/hole-diagram/hazard-landmark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
frontend/public/hole-diagram/hazard-layup.png
Normal file
BIN
frontend/public/hole-diagram/hazard-layup.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/public/hole-diagram/hazard-rock.png
Normal file
BIN
frontend/public/hole-diagram/hazard-rock.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
frontend/public/hole-diagram/hazard-tree.png
Normal file
BIN
frontend/public/hole-diagram/hazard-tree.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3 KiB |
Loading…
Reference in a new issue