<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class MapController extends Controller { // app/Http/Controllers/MapController.php public function show(string $slug = 'groundfloor') { $floor = \DB::table('floors')->where('slug', $slug)->first(); abort_unless($floor, 404); $floors = \DB::table('floors')->orderBy('sort_order')->get(); $areas = \DB::table('areas') ->where('floor_id', $floor->id) ->where('is_active', 1) ->orderBy('name') ->get(); // ---- session state $currentCp = null; $destCp = null; $currentDot = null; $destDot = null; $currentId = session('nav.current_id'); $destSlug = session('nav.dest_slug'); if ($currentId) $currentCp = \DB::table('checkpoints')->find($currentId); if ($destSlug) $destCp = \DB::table('checkpoints')->where('slug', $destSlug)->first(); if ($currentCp) $currentDot = ['x'=>$currentCp->x, 'y'=>$currentCp->y]; if ($destCp) $destDot = ['x'=>$destCp->x, 'y'=>$destCp->y]; // ---- route computation containers $segmentsByFloor = []; // floor_id => ["x1,y1 x2,y2", ...] $routeCpsByFloor = []; // floor_id => [cp, cp, ...] $blinkSegmentPointsByFloor = []; // floor_id => "x1,y1 x2,y2" (trimmed) $routeStepIndex = []; // checkpoint_id => global step number (1..N) $nextCp = null; // next checkpoint after current along path $nextInstruction = null; // human instruction text $ttsSteps = []; // array of short voice lines (we'll speak the first one) // ---- build graph & path if both ends are known if ($currentCp && $destCp) { [$nodes, $adj] = $this->buildGraph(); $pathIds = $this->dijkstra($adj, $currentCp->id, $destCp->id); // global step numbers (1-based) foreach ($pathIds as $i => $id) { $routeStepIndex[$id] = $i + 1; } // collect segments per floor + list of route checkpoints per floor for ($i = 0; $i < count($pathIds) - 1; $i++) { $a = $nodes[$pathIds[$i]] ?? null; $b = $nodes[$pathIds[$i+1]] ?? null; if (!$a || !$b) continue; // collect route cps (ensure uniqueness & order) $routeCpsByFloor[$a->floor_id] = $routeCpsByFloor[$a->floor_id] ?? []; if (empty($routeCpsByFloor[$a->floor_id]) || end($routeCpsByFloor[$a->floor_id])->id !== $a->id) { $routeCpsByFloor[$a->floor_id][] = $a; } $routeCpsByFloor[$b->floor_id] = $routeCpsByFloor[$b->floor_id] ?? []; if (empty($routeCpsByFloor[$b->floor_id]) || end($routeCpsByFloor[$b->floor_id])->id !== $b->id) { $routeCpsByFloor[$b->floor_id][] = $b; } // draw only same-floor segments for the baseline if ($a->floor_id === $b->floor_id) { $segmentsByFloor[$a->floor_id] = ($segmentsByFloor[$a->floor_id] ?? []); $segmentsByFloor[$a->floor_id][] = "{$a->x},{$a->y} {$b->x},{$b->y}"; } } // find next checkpoint after current & highlight the first leg on same floor $idx = array_search($currentCp->id, $pathIds, true); if ($idx !== false && $idx < count($pathIds) - 1) { $nextCp = $nodes[$pathIds[$idx + 1]] ?? null; if ($nextCp) { // Cross-floor? tell user to move floors if ($nextCp->floor_id !== $currentCp->floor_id) { $nextFloor = \DB::table('floors')->where('id', $nextCp->floor_id)->first(); $nextFloorName = $nextFloor->name ?? 'next floor'; $nextInstruction = "Use the stairs or lift to reach {$nextFloorName}, then scan the QR there."; $ttsSteps[] = "Use the stairs or lift to reach {$nextFloorName}."; } else { // Same-floor: trim the end of the blinking segment so the arrow doesn't touch the next checkpoint $nextInstruction = "Walk to {$nextCp->name} and scan the QR there."; $x1 = (float)$currentCp->x; $y1 = (float)$currentCp->y; $x2 = (float)$nextCp->x; $y2 = (float)$nextCp->y; $dx = $x2 - $x1; $dy = $y2 - $y1; $len = sqrt($dx*$dx + $dy*$dy); $gapPx = 14.0; if ($len > 0) { $ux = $dx / $len; $uy = $dy / $len; // unit vector $trim = min($gapPx, max(0.0, $len - 1.0)); // avoid over-trimming $tx = $x2 - $ux * $trim; $ty = $y2 - $uy * $trim; $blinkSegmentPointsByFloor[$currentCp->floor_id] = sprintf('%.0f,%.0f %.0f,%.0f', $x1, $y1, $tx, $ty); } // Build a concise voice line for the first leg. // Optionally hint about the next turn if we have one more segment. $line = "Walk to {$nextCp->name}."; if ($idx + 2 < count($pathIds)) { $b = $nodes[$pathIds[$idx + 1]]; // next $c = $nodes[$pathIds[$idx + 2]]; // next after next // only attempt turn if same floor if ($b && $c && $b->floor_id === $c->floor_id) { $ax = $currentCp->x; $ay = $currentCp->y; $bx = $b->x; $by = $b->y; $cx = $c->x; $cy = $c->y; $turn = $this->turnHint($ax,$ay,$bx,$by,$cx,$cy); if ($turn) $line .= " Then {$turn}."; } } $ttsSteps[] = $line; } } } // If the current is already the destination, say arrived. if (!empty($pathIds) && end($pathIds) === $currentCp->id) { $ttsSteps = ["You have arrived at {$destCp->name}."]; // overrides $nextInstruction = "You have arrived at {$destCp->name}."; } } // ---- all checkpoints on this floor (for context & dropdown) $floorCheckpoints = \DB::table('checkpoints') ->where('floor_id', $floor->id) ->orderBy('name') ->get(['id','slug','name','x','y','type']); // choose a reasonable TTS language; swap to 'ms-MY' if you record Malay names $ttsLang = 'en-US'; return view('map.show', compact( 'floor','floors','areas', 'segmentsByFloor','routeCpsByFloor','blinkSegmentPointsByFloor', 'currentDot','destDot','currentCp','destCp', 'nextCp','nextInstruction','floorCheckpoints','routeStepIndex', 'ttsSteps','ttsLang' )); } /** * Small helper: hint a turn direction at the middle point (b) between (a)->(b)->(c). * Returns 'turn left', 'turn right', or 'continue straight' (or '' if unknown). */ private function turnHint($ax,$ay,$bx,$by,$cx,$cy): string { $v1x = $bx - $ax; $v1y = $by - $ay; $v2x = $cx - $bx; $v2y = $cy - $by; $len1 = sqrt($v1x*$v1x + $v1y*$v1y); $len2 = sqrt($v2x*$v2x + $v2y*$v2y); if ($len1 == 0 || $len2 == 0) return ''; // normalize $v1x /= $len1; $v1y /= $len1; $v2x /= $len2; $v2y /= $len2; // Y-DOWN screen coords: cross>0 means RIGHT, cross<0 means LEFT $cross = $v1x*$v2y - $v1y*$v2x; $dot = $v1x*$v2x + $v1y*$v2y; $angle = atan2($cross, $dot) * 180.0 / M_PI; // only for magnitude // tune thresholds as you like if (abs($angle) > 155) return 'make a U-turn'; if (abs($angle) < 25) return 'continue straight'; return ($cross > 0) ? 'turn right' : 'turn left'; } // ----- Helpers: graph + Dijkstra ----- private function buildGraph(): array { $nodes = DB::table('checkpoints')->get()->keyBy('id'); $edges = DB::table('checkpoint_links')->get(); $adj = []; foreach ($edges as $e) { if (!isset($nodes[$e->from_id]) || !isset($nodes[$e->to_id])) continue; $from = $nodes[$e->from_id]; $to = $nodes[$e->to_id]; $w = $e->weight ?? $this->euclid($from->x, $from->y, $to->x, $to->y); $adj[$e->from_id][] = [$e->to_id, $w]; if ($e->bidirectional) { $adj[$e->to_id][] = [$e->from_id, $w]; } } return [$nodes, $adj]; } private function euclid($x1,$y1,$x2,$y2): float { $dx = $x1 - $x2; $dy = $y1 - $y2; return sqrt($dx*$dx + $dy*$dy); } private function dijkstra(array $adj, int $start, int $goal): array { $dist = []; $prev = []; $q = []; foreach ($adj as $u => $_) { $dist[$u] = INF; $prev[$u] = null; $q[$u] = true; } // ensure start exists even if it has no outgoing edges $dist[$start] = 0; $q[$start] = true; while (!empty($q)) { // pick u in q with smallest dist $u = null; $min = INF; foreach ($q as $id => $_) { if (isset($dist[$id]) && $dist[$id] < $min) { $min = $dist[$id]; $u = $id; } } if ($u === null) break; unset($q[$u]); if ($u === $goal) break; // done if (!isset($adj[$u])) continue; foreach ($adj[$u] as [$v, $w]) { $alt = $dist[$u] + $w; if (!isset($dist[$v]) || $alt < $dist[$v]) { $dist[$v] = $alt; $prev[$v] = $u; $q[$v] = true; // ensure v is in queue } } } // reconstruct $path = []; $u = $goal; if (!isset($prev[$u]) && $u !== $start) { return [$start]; // no path found } while ($u !== null) { array_unshift($path, $u); $u = $prev[$u] ?? null; } return $path; } public function coords(string $slug) { $floor = \DB::table('floors')->where('slug', $slug)->first(); abort_unless($floor, 404); $checkpoints = \DB::table('checkpoints') ->where('floor_id', $floor->id) ->orderBy('name') ->get(['id','name','slug','x','y','type']); return view('map.coords', [ 'floor' => $floor, 'checkpoints' => $checkpoints, // suggestions you’ll see in the name field (edit as you like) 'suggestions' => match ($slug) { 'groundfloor' => ['Natural Resources & Economy Gallery'], 'floor1' => ['Entrance (F1)','Textile Gallery','Temporary Gallery','Islamic Gallery','Petroleum Gallery','Junction near Information'], 'floor2' => ['Craft Gallery','Royal Gallery'], 'floor3' => ['History Gallery','Bridge'], default => [], }, ]); } }