From f3b3d30e4e3185da5937b78a70cb736985ad774d Mon Sep 17 00:00:00 2001 From: Erol Haagenrud Date: Tue, 18 Aug 2026 06:43:57 +0200 Subject: [PATCH] Hull-diagram v2: rettelser fra ekte produksjonsbruk (ADR-084 del 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ARCHITECTURE_DECISIONS.md | 93 ++++++++++ CHANGELOG.md | 70 +++++++ frontend/components/hole-diagram-view.tsx | 175 ++++++++++++++---- frontend/components/hole-target-distance.tsx | 23 ++- frontend/public/hole-diagram/hazard-creek.png | Bin 0 -> 2941 bytes .../public/hole-diagram/hazard-dogleg.png | Bin 0 -> 2874 bytes .../public/hole-diagram/hazard-landmark.png | Bin 0 -> 2920 bytes frontend/public/hole-diagram/hazard-layup.png | Bin 0 -> 2901 bytes frontend/public/hole-diagram/hazard-rock.png | Bin 0 -> 2905 bytes frontend/public/hole-diagram/hazard-tree.png | Bin 0 -> 3032 bytes 10 files changed, 324 insertions(+), 37 deletions(-) create mode 100644 frontend/public/hole-diagram/hazard-creek.png create mode 100644 frontend/public/hole-diagram/hazard-dogleg.png create mode 100644 frontend/public/hole-diagram/hazard-landmark.png create mode 100644 frontend/public/hole-diagram/hazard-layup.png create mode 100644 frontend/public/hole-diagram/hazard-rock.png create mode 100644 frontend/public/hole-diagram/hazard-tree.png diff --git a/ARCHITECTURE_DECISIONS.md b/ARCHITECTURE_DECISIONS.md index 22d6e6c..231e62e 100644 --- a/ARCHITECTURE_DECISIONS.md +++ b/ARCHITECTURE_DECISIONS.md @@ -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) diff --git a/CHANGELOG.md b/CHANGELOG.md index f31695e..e96c7b2 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/frontend/components/hole-diagram-view.tsx b/frontend/components/hole-diagram-view.tsx index d6bcbba..26f268a 100644 --- a/frontend/components/hole-diagram-view.tsx +++ b/frontend/components/hole-diagram-view.tsx @@ -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 = { + 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 - } - if (kind === "water") { - return +const _ICON_SRC: Partial> = { + 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 } return } /** 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 (
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",
+ {behindGreenHazards.map((h) => ( + + ))} + )} @@ -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 (
  • -
  • ) } @@ -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}.` } diff --git a/frontend/components/hole-target-distance.tsx b/frontend/components/hole-target-distance.tsx index a68f077..0af53b4 100644 --- a/frontend/components/hole-target-distance.tsx +++ b/frontend/components/hole-target-distance.tsx @@ -54,17 +54,26 @@ const _HAZARD_LABELS: Record = { 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 = { 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 diff --git a/frontend/public/hole-diagram/hazard-creek.png b/frontend/public/hole-diagram/hazard-creek.png new file mode 100644 index 0000000000000000000000000000000000000000..d45baf3469b1957d163fdf295ea2130d1e4165a4 GIT binary patch literal 2941 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 literal 0 HcmV?d00001 diff --git a/frontend/public/hole-diagram/hazard-dogleg.png b/frontend/public/hole-diagram/hazard-dogleg.png new file mode 100644 index 0000000000000000000000000000000000000000..023da74b66a74ac3ab28d2eb2597232dc6f6113d GIT binary patch literal 2874 zcmV-A3&r$_P)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H13dc!A zK~z|UotJBjT<2NGfA6`>Idi}7>~(g1S$k~i?%rp2c4p3;GiT2I; zC_pU!pC3KXPtQgAKSD~$y{Oltq-p*aWD7Z5dx%);zahP6pN}VzL_NH zzsu)o-%DQcQ2>Dx9QKm%8>%Xfd!fVf`n#;PZqsvi@dFP?FjE=I`8wl`J?x)6L`pG{ zlIw{SUn~?dSN>lBmPJ_+?sJMJ9$MR8;_ADXX?52Kf`px23)}ICqJ$`pNSg-PObR0% zQz@318kyzj>%MWw{R&iJ*>NfLR~wyA0)%2yFm zEUex_6(&YjK}#tN1CM;Rz|{C8NLW~V2SpsGZTI=yXGXAHjqS<^rMyb$$B4)QrA!p* z9NRa|o{1V8ogStc5QZ_+jcHsrAd}AX_R=*TI(9~=YVc((2!eBH2^ZeJ#8{=lZof?s z1W#Z6CF6w4tr@Z+=AqhaR42xXl@jyIefEw;1a5#-G$tDbdaj4A#iUZB zEUs_k3=~@3ZEmg%$eKA~V!WaEq!z{DjOBJn8yVXDZ2PcoWTz~5~QNZSPE57G4Mk6PL89h5`;?D z%+n~3vf93RcL#c2n#r`uC#HbtHa~l2j=;Ww877l7v!YE|4(jbviYC@=X zqcT#GK(){$m(~d)n_<|+k39B{&*64MCd=cbOq+p!lifg4&W%y1J;bf~b?TEE>0}I5 z%`-JOpyzKf@P0f3LoMLADpwZXL`FHPg&YT`j#0^1NkVMR)U_4Q9hMGGA4I{&^S6nG zM_QSnpsN%#ov}S>P8|4o25yI|OaDT>SR! z!xFc*eni$R(zgSW*hCUcYgkdS!%iu}s}a zvDxYJsnbv3xjwmS{y9a6^j|27nml;qBtsu!U$B3=OnQT`Z@=+WHno%EWeERyJ0MaVeC_80nCK z-J{=4D3uCKOq7s{pwkJsesh6?2M#eZQbE&GtZc6l_&J_B`y`I#P^r|emdjWgfTrtT zl9G#0f8sN|^v0{$ZUbHGlGetE;#C$`uM&o70zY7D>o%5s3q^AopQw>3h8%39@S+w0 zT{5W(z1U*^;k~G;!rIOqMvBvlU$V5m z!r%P%Ut&m|k=ppnBuN57p#0d~{rBxhBvNN>;|{8>ql+@#zT`K5;n&EkCWT_fHH_MLxs3H8Q`+x` zr)+!og69o1#nAckAOAY1kM5(8+eXTmdT|D8&_)pkiIgBCWFl~V2Q5|S%Ju6sY7;5`kx!O?;5ywdKrise$7TWh!Z&P&hpgV!&xf1<{ou`>B=8X+V} z47>Ia+YMOX>C$st9=`wMeC~bV=(uB;g)_zCJv=7wQ?}FTf7i0Cv(CU0 zl}eQ$^!eef8{AmF#b$epcCUk!l96hSW@CccsTn?YOPeE=Cf)3@vsU8|?4ioh@p^i+zJu8~NHqlhpJab1^0LaA6x^7+Essg&{6 zT+TfIf93y3fcIqERFlMgTFT@p&vTE3VVH~KL{k)ms_3zzs;;5yx3!e{Z6VCB-Rpb) YUvb2x{o>iUmjD0&07*qoM6N<$f~}2@BLDyZ literal 0 HcmV?d00001 diff --git a/frontend/public/hole-diagram/hazard-landmark.png b/frontend/public/hole-diagram/hazard-landmark.png new file mode 100644 index 0000000000000000000000000000000000000000..8ae5440c3552c8ff68ae4e1f0f095e2ae67b56c2 GIT binary patch literal 2920 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 literal 0 HcmV?d00001 diff --git a/frontend/public/hole-diagram/hazard-rock.png b/frontend/public/hole-diagram/hazard-rock.png new file mode 100644 index 0000000000000000000000000000000000000000..84ade954424785f7f7e6198df2762ba75524f6a9 GIT binary patch literal 2905 zcmV-f3#RmmP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H13g$^f zK~z|UotIgR-q%^if9Jn{`!>rr-)#2SV~@vMY{yPZlA0J3LJDdn0!=rR(ln}|Tp*NI z<)Rh6L89d%tpvG=dPBIN(2xZPX&Nge4N2_8cE-zW-e&*i+n4{oabYD$g)|Y*_0e;F z=ar7$=Mixn^D&ihoZjaV>gN&c6Ot$jffmOI0d^eIB#!6eh^Gz1cor}}j;Z)40M~V= zV&W%-#C_Mc7L<+p3d`$PX!Pnho_(84PcodZFgiHF%=ke@iW4DG^e>(pd_9xN)c?N# zy5l-Oj79k9oAa+quf6vYmSdxf0`*pxMypNVGC>HEMuJiyPbrrslhqg~46=9p0LS;- z9TM||t|lG_qK^&W`{o@{EI)U1dujY9ul|^>X;ItuIDhpLeXGyJ$QYgr-jT<-@5XjnU~-B zR~%R2t@9Vji#0SP@xtyQDi+(MFM{J>{+$36@riHJc+g34flQZdE7hklcz z(JIg!ywLQm~%npK(3F8PuSJ~XEad!R^ zZ>*nXy}I$i!|vR7n0!v*kx&0Iv*U;64c#~a^Z=Ci5800SqmJF#{n~{y=u!#SyTx6H zjzKKAa`O_~?G1+VgM9npKSP)?s;prc34*{z1OXz}dE>2juwonCP&lw>FKHt~5Jo7n zgsNnD@r~!1`0S3EC=C84nM^uc&(v&y-Ji@2VPqN*I;dw@G;4r2O!kOTBckWZ(P zB$;c=i!3f*hZu$iOZ59Dgy>vPA%*`1TK+Us>kOxz}l%P2Kan?*d5R zOh^*dF-^Ub{f2-)2u`8belS*VjmBGUdt`d#A>U>=w^F z^DN6tOIW5!-|SuQrTG^B@rdKaPBSMy?B+eiBVM5 zKr>Q6i0e7@ESFxdkEThC4i{Kly2jeZHaBjpAY6q?VSuB14>DL7M%86zcg(Qas?zMY zk!4Yoq6lGmHhrg$kuW%UeQEaF>VHhGw zA#3#&%7rmxVkV3+yk4C!2uP(f$U@NTc4;eePH8J1SJ zNoMmnu16FFY*n{t*6WOoS2%vh454IjYompvXf)d!7;=mvX{d@s5KHtoeZ~j&;PyO( zE^%#bgzawEI<*oJOtPpr6vI*0woZ86^rm5YX+} zoV@o0*-VCxN3Ge!k3BkVAG6k`I5vnM227SGkdW3C zP16bM^CF1HWBfAa~_(7~``ioLJoqjIxgZhIf?#ED5l=G8l zipHvao%8m8(MolQWWh$Y&d<)gj;cV?kWjOE>dhA2#umM9hslX4y4^l~)1=!s3F4TO zCw~KzHhNa#)ag?Mx?pW{lT$}e0-~BoBwkhkJDEzoY?=Ls5A8m{EAKQ|vKN`1m|tUHr)uQP(D9Typ7H^VFN^EYn5VFDQ<{H`gB0Hz|p{g>zAF{ex$B}y+I5CZ^ zDlDwt!09CT>Sw-;?|5W0xidgV#Bq%4x_dh9=G%&{k2)~ z;i+#wfnz!Ck&(&)VEuzc)f@G1**N0M-}@8x?HZ+HuQ4__&h@R!2uW~g=V2Dt z=ZND7ghWQmqstn0VB!Q8d96%Ao}$y}fQU%tHRP1UcFTo8-&_`1EiAacT1(xJ%IhcLaFC@`+B|Ji>B3^s5x!E z@%_JGpin?dcIaAlWJO{3$P|rU6-kn?U5nkLds$qYXCOC>5HcAf%Z;rYNU?!uWw0EV zC%^gEsDVT}ow}aMyEZ?0<$2D$`wO<)4R%jVkV&)L=Vxzu| zY2WUjNh8VF&Bo7{x;k2{Nn(;FEt2)yDiK5^(cz& zjN`ZxhG9wwfh0+NMNz7XqP(Z7>MM$(ya{waj_H2`b!uP5jf9EB00000NkvXXu0mjf D%ZZj) literal 0 HcmV?d00001 diff --git a/frontend/public/hole-diagram/hazard-tree.png b/frontend/public/hole-diagram/hazard-tree.png new file mode 100644 index 0000000000000000000000000000000000000000..bfca5e697d040d495faeb40af52d574d1f08d025 GIT binary patch literal 3032 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