[Back] <?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 => [],
},
]);
}
}