"use client" import { useEffect, useRef, useState } from "react" import { TargetDistance } from "@/components/target-distance" import { HoleDiagram } from "@/components/hole-diagram-view" import { haversineMeters, type LatLng } from "@/lib/geo" import { midpoint, projectOntoAxis } from "@/lib/hole-geometry" // Rangefinder-datalag (ADR-064) -- henter rundens rå banepunkter (green // front/senter/bak, hindringer) og kombinerer dem klient-side med spillerens // LIVE GPS-posisjon via samme watchPosition-mønster som map-point-picker.tsx // allerede bruker for slagmåling (ADR-048 tillegg 2026-08-08 del 2) -- // `enableHighAccuracy`, grasiøs degradering ved avslått tillatelse, // `clearWatch` ved avmontering. `TargetDistance` selv forblir "dum" // (V0-eksportert, ren visning) -- all logikk bor her. Ingen egen // rad-wrapper (border/padding) her -- kalleren styrer plassering/bredde. // Montert i PlayerHoleCards sin hull-navigator (round-detail.tsx), // size="full" -- IKKE i ScoringWizard-headeren (der den satt først, // 2026-08-12): brukeren pekte ut hovedvisningen som riktig sted og ba // om mer bredde enn den opprinnelige kompaktvarianten, 2026-08-13. // // `baseUrl` (ikke `roundId`) siden migrasjon 079 -- komponenten gjenbrukes // nå av tre kallesteder med tre ulike URL-familier (frittstående runde // `/rounds/{id}`, individuell org-turnering `/orgs/{org}/tournaments/{t}/ // rounds/{r}`, lag-matchplay `/orgs/{org}/matches/{m}`) -- kalleren bygger // selv `${baseUrl}/holes/{n}/target-points`, komponenten trenger ikke vite // hvilken flyt den sitter i. // // Strømsparing (2026-08-13, brukerrapport om høyt batteriforbruk): // (1) `maximumAge` satt til 4000ms (var 1000ms) -- en golfer beveger seg // ~1,5 m/s, oppdatering hvert 4. sekund er umerkelig i praksis men ber // GPS-brikken om et ferskt fix mye sjeldnere. (2) `watchPosition` pauses // helt når fanen/appen ikke er synlig (`document.visibilitychange`) -- // ingen vits å holde GPS-en aktiv for en skjerm ingen ser på (skjerm låst, // annen app i forgrunnen), og gjenopptas automatisk når brukeren kommer // tilbake. `enableHighAccuracy` beholdt UENDRET -- lav nøyaktighet ville // gitt titalls meters feilmargin, for upresist til at front/midt/bak- // tallene gir mening. type ApiTargetPoint = { poi_type: string location: "front" | "middle" | "back" | null side_fairway: "left" | "center" | "right" | null latitude: number longitude: number } const _HAZARD_LABELS: Record = { green_bunker: "Bunker (green)", fairway_bunker: "Bunker (fairway)", water: "Vannhinder", rock: "Fjellknaus", } export function HoleTargetDistance({ baseUrl, holeNumber, size = "compact", className, }: { baseUrl: string holeNumber: number size?: "compact" | "full" className?: string }) { const [points, setPoints] = useState(null) const [livePosition, setLivePosition] = useState(null) const watchIdRef = useRef(null) useEffect(() => { let cancelled = false setPoints(null) fetch(`${baseUrl}/holes/${holeNumber}/target-points`, { credentials: "include" }) .then((res) => (res.ok ? res.json() : [])) .then((data: ApiTargetPoint[]) => { if (!cancelled) setPoints(data) }) .catch(() => { if (!cancelled) setPoints([]) }) return () => { cancelled = true } }, [baseUrl, holeNumber]) useEffect(() => { // Ingen vits å spore GPS når banen uansett ikke har koordinatdata for // dette hullet -- unngår unødvendig batteribruk/tillatelses-prompt. if (!points || points.length === 0) return if (typeof navigator === "undefined" || !navigator.geolocation) return if (typeof document === "undefined") return function startWatch() { watchIdRef.current = navigator.geolocation.watchPosition( (pos) => { setLivePosition({ lat: pos.coords.latitude, lng: pos.coords.longitude }) }, () => { // Avslått/feilet -- degraderer grasiøst, komponenten viser bare "ingen data". }, { enableHighAccuracy: true, maximumAge: 4000 }, ) } function stopWatch() { if (watchIdRef.current !== null && typeof navigator !== "undefined" && navigator.geolocation) { navigator.geolocation.clearWatch(watchIdRef.current) } watchIdRef.current = null } // Pauser GPS-sporingen helt når fanen/appen ikke er synlig (skjerm // låst, annen app i forgrunnen) -- gjenopptas automatisk når brukeren // kommer tilbake. function handleVisibilityChange() { if (document.hidden) { stopWatch() } else { startWatch() } } if (!document.hidden) startWatch() document.addEventListener("visibilitychange", handleVisibilityChange) return () => { document.removeEventListener("visibilitychange", handleVisibilityChange) stopWatch() } }, [points]) // Fortsatt laster, ELLER banen har rett og slett ingen koordinatdata // (det store flertallet av runder, TeeOff-baner har ingen GolfAPI-kobling) // -- ingen synlig rad i det hele tatt, ikke en "ingen data"-lapp på HVER // eneste runde. Lappen (TargetDistance sin egen null-tilstand) er forbeholdt // "banen HAR data, men venter på GPS-posisjon ennå" -- da vet vi det er // verdt å vise noe. if (points === null || points.length === 0) return null if (!livePosition) { return } const greenPoints = points.filter((p) => p.poi_type === "green" && p.location) const front = greenPoints.find((p) => p.location === "front") const middle = greenPoints.find((p) => p.location === "middle") const back = greenPoints.find((p) => p.location === "back") if (!front || !middle || !back) { return } const distances = { front: Math.round(haversineMeters(livePosition, { lat: front.latitude, lng: front.longitude })), middle: Math.round(haversineMeters(livePosition, { lat: middle.latitude, lng: middle.longitude })), back: Math.round(haversineMeters(livePosition, { lat: back.latitude, lng: back.longitude })), } // ADR-083: ALLE hindringer, ikke bare nærmeste (tidligere reduserte denne // løkken til ett enkelt nearestHazard-objekt) -- sortert nærmest // spilleren først. Avstandene er ALLTID live fra spilleren, uavhengig av // om et geometrisk diagram kan tegnes under. const hazardPoints = points.filter((p) => p.poi_type in _HAZARD_LABELS) const hazards = hazardPoints .map((h) => { const base = _HAZARD_LABELS[h.poi_type] const label = h.location === "front" || h.location === "back" ? `${base} (${h.location === "front" ? "front" : "bak"})` : base return { label, distanceMeters: Math.round(haversineMeters(livePosition, { lat: h.latitude, lng: h.longitude })), lat: h.latitude, lng: h.longitude } }) .sort((a, b) => a.distanceMeters - b.distanceMeters) // Fast tee-referanse (IKKE spillerens bevegelige livePosition) for selve // hull-diagrammet -- se ADR-083. Midtpunkt hvis begge tee_front/tee_back // finnes, ellers den ene som finnes. Mangler begge (forekommer, ikke alle // baner har tee-koordinater): faller tilbake til den flate listen under, // ingen regresjon. const teeFront = points.find((p) => p.poi_type === "tee_front") const teeBack = points.find((p) => p.poi_type === "tee_back") const teeRef = teeFront && teeBack ? midpoint({ lat: teeFront.latitude, lng: teeFront.longitude }, { lat: teeBack.latitude, lng: teeBack.longitude }) : teeFront ? { lat: teeFront.latitude, lng: teeFront.longitude } : teeBack ? { lat: teeBack.latitude, lng: teeBack.longitude } : null if (teeRef) { const greenRef = { lat: middle.latitude, lng: middle.longitude } const player = projectOntoAxis(teeRef, greenRef, livePosition) const diagramHazards = hazards.map((h) => ({ ...h, ...projectOntoAxis(teeRef, greenRef, { lat: h.lat, lng: h.lng }), })) return ( ) } return ( ) }