2026-08-12 21:39:13 +02:00
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
import { useEffect, useRef, useState } from "react"
|
|
|
|
|
import { TargetDistance } from "@/components/target-distance"
|
|
|
|
|
import { haversineMeters, type LatLng } from "@/lib/geo"
|
|
|
|
|
|
2026-08-14 11:12:25 +02:00
|
|
|
// Rangefinder-datalag (ADR-064) -- henter rundens rå banepunkter (green
|
|
|
|
|
// front/senter/bak, hindringer) og kombinerer dem klient-side med spillerens
|
2026-08-12 21:39:13 +02:00
|
|
|
// 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) --
|
2026-08-13 15:01:59 +02:00
|
|
|
// `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
|
2026-08-13 10:48:09 +02:00
|
|
|
// 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.
|
2026-08-13 15:01:59 +02:00
|
|
|
//
|
|
|
|
|
// 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.
|
2026-08-12 21:39:13 +02:00
|
|
|
|
|
|
|
|
type ApiTargetPoint = {
|
|
|
|
|
poi_type: string
|
|
|
|
|
location: "front" | "middle" | "back" | null
|
|
|
|
|
side_fairway: "left" | "center" | "right" | null
|
|
|
|
|
latitude: number
|
|
|
|
|
longitude: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const _HAZARD_LABELS: Record<string, string> = {
|
2026-08-13 21:24:25 +02:00
|
|
|
green_bunker: "Bunker (green)",
|
2026-08-12 21:39:13 +02:00
|
|
|
fairway_bunker: "Bunker (fairway)",
|
|
|
|
|
water: "Vannhinder",
|
2026-08-13 21:24:25 +02:00
|
|
|
rock: "Fjellknaus",
|
2026-08-12 21:39:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function HoleTargetDistance({
|
|
|
|
|
roundId,
|
|
|
|
|
holeNumber,
|
|
|
|
|
size = "compact",
|
|
|
|
|
className,
|
|
|
|
|
}: {
|
|
|
|
|
roundId: string
|
|
|
|
|
holeNumber: number
|
|
|
|
|
size?: "compact" | "full"
|
|
|
|
|
className?: string
|
|
|
|
|
}) {
|
|
|
|
|
const [points, setPoints] = useState<ApiTargetPoint[] | null>(null)
|
|
|
|
|
const [livePosition, setLivePosition] = useState<LatLng | null>(null)
|
|
|
|
|
const watchIdRef = useRef<number | null>(null)
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
let cancelled = false
|
|
|
|
|
setPoints(null)
|
|
|
|
|
fetch(`/rounds/${roundId}/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
|
|
|
|
|
}
|
|
|
|
|
}, [roundId, 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
|
2026-08-13 15:01:59 +02:00
|
|
|
if (typeof document === "undefined") return
|
2026-08-12 21:39:13 +02:00
|
|
|
|
2026-08-13 15:01:59 +02:00
|
|
|
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() {
|
2026-08-12 21:39:13 +02:00
|
|
|
if (watchIdRef.current !== null && typeof navigator !== "undefined" && navigator.geolocation) {
|
|
|
|
|
navigator.geolocation.clearWatch(watchIdRef.current)
|
|
|
|
|
}
|
|
|
|
|
watchIdRef.current = null
|
|
|
|
|
}
|
2026-08-13 15:01:59 +02:00
|
|
|
|
|
|
|
|
// Pauser GPS-sporingen helt når fanen/appen ikke er synlig (skjerm
|
|
|
|
|
// låst, annen app i forgrunnen) -- gjenopptas automatisk når brukeren
|
2026-08-14 11:12:25 +02:00
|
|
|
// kommer tilbake.
|
2026-08-13 15:01:59 +02:00
|
|
|
function handleVisibilityChange() {
|
|
|
|
|
if (document.hidden) {
|
|
|
|
|
stopWatch()
|
|
|
|
|
} else {
|
|
|
|
|
startWatch()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!document.hidden) startWatch()
|
|
|
|
|
document.addEventListener("visibilitychange", handleVisibilityChange)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
document.removeEventListener("visibilitychange", handleVisibilityChange)
|
|
|
|
|
stopWatch()
|
|
|
|
|
}
|
2026-08-12 21:39:13 +02:00
|
|
|
}, [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) {
|
2026-08-14 11:12:25 +02:00
|
|
|
return <TargetDistance size={size} distances={null} className={className} />
|
2026-08-12 21:39:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-08-14 11:12:25 +02:00
|
|
|
return <TargetDistance size={size} distances={null} className={className} />
|
2026-08-12 21:39:13 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 })),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hazards = points.filter((p) => p.poi_type in _HAZARD_LABELS)
|
|
|
|
|
let nearestHazard: { label: string; distanceMeters: number } | null = null
|
|
|
|
|
for (const h of hazards) {
|
|
|
|
|
const d = haversineMeters(livePosition, { lat: h.latitude, lng: h.longitude })
|
|
|
|
|
if (nearestHazard === null || d < nearestHazard.distanceMeters) {
|
|
|
|
|
const base = _HAZARD_LABELS[h.poi_type]
|
|
|
|
|
const label = h.location === "front" || h.location === "back" ? `${base} (${h.location === "front" ? "front" : "bak"})` : base
|
|
|
|
|
nearestHazard = { label, distanceMeters: Math.round(d) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
2026-08-14 11:12:25 +02:00
|
|
|
<TargetDistance size={size} distances={distances} nearestHazard={nearestHazard} className={className} />
|
2026-08-12 21:39:13 +02:00
|
|
|
)
|
|
|
|
|
}
|