diff --git a/app/Http/Controllers/AdminBukuController.php b/app/Http/Controllers/AdminBukuController.php index 5613eca..845181a 100644 --- a/app/Http/Controllers/AdminBukuController.php +++ b/app/Http/Controllers/AdminBukuController.php @@ -34,6 +34,10 @@ public function store(Request $request) $validated['cover'] = $request->file('cover')->store('covers', 'public'); } + $koordinat = $this->tentukanKoordinatRak($validated['nomor_panggil']); + $validated['lokasi_x'] = $koordinat['x']; + $validated['lokasi_y'] = $koordinat['y']; + $validated['konten_digital'] = 0; Buku::create($validated); @@ -88,6 +92,10 @@ public function update(Request $request, $id) $validated['cover'] = $request->file('cover')->store('covers', 'public'); } + $koordinat = $this->tentukanKoordinatRak($validated['nomor_panggil']); + $validated['lokasi_x'] = $koordinat['x']; + $validated['lokasi_y'] = $koordinat['y']; + $buku->update($validated); return redirect()->route('admin.buku.index')->with('success', 'Aset buku berhasil diperbarui.'); @@ -100,4 +108,52 @@ public function destroy($id) return redirect()->route('admin.buku.index')->with('success', 'Aset buku berhasil dihapus dari sistem.'); } + + private function tentukanKoordinatRak($nomor_panggil) + { + if (empty($nomor_panggil)) return ['x' => null, 'y' => null]; + + $kode_utama = (int) substr(trim(preg_replace('/[^0-9]/', '', $nomor_panggil)), 0, 3); + + return match (true) { + $kode_utama >= 0 && $kode_utama <= 99 => match (true) { + $kode_utama <= 19 => ['x' => 13.00, 'y' => 22.00], // Rak 01 + $kode_utama <= 50 => ['x' => 18.00, 'y' => 22.00], // Rak 02 + default => ['x' => 25.00, 'y' => 22.00], // Rak 03-05 + }, + $kode_utama >= 100 && $kode_utama <= 199 => match (true) { + $kode_utama <= 150 => ['x' => 43.00, 'y' => 20.00], // Rak 06-10 + default => ['x' => 57.00, 'y' => 20.00], // Rak 11-14 + }, + $kode_utama >= 200 && $kode_utama <= 299 => match (true) { + $kode_utama == 297 => ['x' => 25.00, 'y' => 38.00], // Rak 25-32 (Islam) + default => ['x' => 13.00, 'y' => 38.00], // Rak 15-24 + }, + $kode_utama >= 300 && $kode_utama <= 399 => match (true) { + $kode_utama <= 330 => ['x' => 43.00, 'y' => 30.00], // Rak 33-36 + $kode_utama <= 360 => ['x' => 50.00, 'y' => 30.00], // Rak 37-40 + default => ['x' => 57.00, 'y' => 30.00], // Rak 41-44 + }, + $kode_utama >= 400 && $kode_utama <= 499 => ['x' => 18.00, 'y' => 62.00], // Rak 45 + $kode_utama >= 500 && $kode_utama <= 599 => ['x' => 50.00, 'y' => 74.00], // Rak 46-48 + $kode_utama >= 600 && $kode_utama <= 699 => match (true) { + $kode_utama <= 610 => ['x' => 35.00, 'y' => 85.00], // Rak 49-53 + $kode_utama <= 630 => ['x' => 45.00, 'y' => 85.00], // Rak 54-58 + $kode_utama <= 650 => ['x' => 55.00, 'y' => 85.00], // Rak 59-63 + default => ['x' => 65.00, 'y' => 85.00], // Rak 64-68 + }, + $kode_utama >= 700 && $kode_utama <= 799 => match (true) { + $kode_utama <= 739 => ['x' => 77.00, 'y' => 22.00], // Rak 71 + $kode_utama <= 769 => ['x' => 82.00, 'y' => 22.00], // Rak 72 + $kode_utama <= 789 => ['x' => 87.00, 'y' => 22.00], // Rak 73 + default => ['x' => 82.00, 'y' => 22.00], // Rak 74 + }, + $kode_utama >= 800 && $kode_utama <= 899 => ['x' => 82.00, 'y' => 32.00], // Rak 77-79 + $kode_utama >= 900 && $kode_utama <= 999 => match (true) { + $kode_utama <= 919 => ['x' => 77.00, 'y' => 42.00], // Rak 69-70 + default => ['x' => 87.00, 'y' => 42.00], // Rak 80-84 + }, + default => ['x' => null, 'y' => null], + }; + } } diff --git a/app/Http/Controllers/AdminPeminjamanController.php b/app/Http/Controllers/AdminPeminjamanController.php index 9743d57..702460a 100644 --- a/app/Http/Controllers/AdminPeminjamanController.php +++ b/app/Http/Controllers/AdminPeminjamanController.php @@ -33,14 +33,14 @@ public function index(Request $request) $peminjaman = $query->paginate(15)->appends(['search' => $search]); $buku = Buku::orderBy('judul', 'asc')->get(); - $anggota = Anggota::orderBy('nama', 'asc')->get(); + $anggota = Anggota::latest()->get(); return view('admin.peminjaman.index', compact('peminjaman', 'buku', 'anggota', 'search')); } public function create() { $buku = Buku::where('eksemplar', '>', 0)->orderBy('judul', 'asc')->get(); - $anggota = Anggota::orderBy('nama', 'asc')->get(); + $anggota = Anggota::latest()->get(); return view('admin.peminjaman.create', compact('buku', 'anggota')); } @@ -48,7 +48,7 @@ public function edit($id) { $peminjaman = Peminjaman::findOrFail($id); $buku = Buku::all(); - $anggota = Anggota::orderBy('nama', 'asc')->get(); + $anggota = Anggota::latest()->get(); return view('admin.peminjaman.edit', compact('peminjaman', 'buku', 'anggota')); } diff --git a/app/Http/Controllers/PeminjamanController.php b/app/Http/Controllers/PeminjamanController.php index 9a20d49..b432499 100644 --- a/app/Http/Controllers/PeminjamanController.php +++ b/app/Http/Controllers/PeminjamanController.php @@ -31,13 +31,13 @@ public function index(\Illuminate\Http\Request $request) $peminjaman = $query->paginate(15)->appends(['search' => $search]); $buku = Buku::where('eksemplar', '>', 0)->get(); - $anggota = Anggota::all(); + $anggota = Anggota::latest()->get(); return view('admin.peminjaman.index', compact('peminjaman', 'buku', 'anggota', 'search')); } public function create() { - $anggota = Anggota::all(); + $anggota = Anggota::latest()->get(); $bukus = Buku::all(); return view('admin.peminjaman.create', compact('anggota', 'bukus')); } diff --git a/app/Http/Controllers/VisitorKatalogController.php b/app/Http/Controllers/VisitorKatalogController.php index 3d8e120..5c12c35 100644 --- a/app/Http/Controllers/VisitorKatalogController.php +++ b/app/Http/Controllers/VisitorKatalogController.php @@ -51,21 +51,12 @@ public function index(Request $request) $hasil = []; foreach ($semuaBuku as $b) { - // Hitung nilai Cosine Similarity untuk masing-masing atribut berdasarkan kata kunci yang sudah dibersihkan - $judulSimilarity = $this->calculateSimilarity($processedSearch, $b->judul); - $pengarangSimilarity = $this->calculateSimilarity($processedSearch, $b->pengarang); - - // Gabungkan penerbit, deskripsi, dan kategori sebagai atribut teks tambahan - $teksTambahan = trim(($b->penerbit ?? '') . ' ' . ($b->deskripsi ?? '') . ' ' . ($b->kategori->nama_kategori ?? '')); - $tambahanSimilarity = $this->calculateSimilarity($processedSearch, $teksTambahan); - - // Penyesuaian Bobot WTS (Weighting) - // Judul (70%), Pengarang (20%), Penerbit/Deskripsi/Kategori (10%) - $totalScore = ($judulSimilarity * 0.7) + ($pengarangSimilarity * 0.2) + ($tambahanSimilarity * 0.1); + // Hitung nilai kemiripan dengan algoritma Dynamic Token-wise WTS + $totalScore = $this->calculateDynamicWTS($processedSearch, $b); // 3. Penetapan Batas Relevansi (Threshold) - // Hanya tampilkan yang memiliki Total Similarity Score >= 0.3 (30%) - if ($totalScore >= 0.3) { + // Hanya tampilkan yang memiliki Total Similarity Score >= 0.2 (20%) + if ($totalScore >= 0.2) { $b->similarity_score = $totalScore; $hasil[] = $b; } @@ -90,44 +81,77 @@ public function index(Request $request) } /** - * Helper untuk menghitung nilai perbandingan kemiripan teks. - * Menggunakan pendekatan Cosine Similarity yang dimodifikasi (Query Coverage) - * agar panjang judul buku tidak menurunkan skor secara drastis - * pada pencarian kata kunci pendek. + * [ALGORITMA DYNAMIC TOKEN-WISE WTS] + * Dibuat khusus untuk memecahkan masalah "Length Penalty" pada Global Search Bar. + * + * KONTEKS MASALAH (Untuk Sidang TA): + * Algoritma Cosine Similarity standar membandingkan string secara utuh. + * Saat user menggabungkan "Judul + Pengarang" (contoh: "Aku Anak Mandiri Watiek"), + * panjang vektor query membesar. Saat dicocokkan HANYA ke kolom Judul ("Aku Anak Mandiri"), + * kata "Watiek" dianggap sebagai noise/sampah, sehingga skor kemiripan Judul anjlok. + * Akibatnya total WTS turun dan buku yang relevan malah tenggelam. + * + * SOLUSI (Tokenisasi Dinamis Lintas Atribut): + * 1. Query dipecah menjadi token (kata per kata). + * 2. Setiap token dievaluasi secara independen ke semua atribut buku (Judul, Pengarang, Kategori). + * 3. Token berhak memilih skor kemiripan TERTINGGI dari atribut manapun (Local Max Pooling). + * -> Contoh: Kata "Watiek" tidak akan menghukum skor Judul, melainkan diidentifikasi + * sebagai milik "Pengarang" dan menyumbang skor 0.3. + * 4. Agregasi: Skor semua token dijumlahkan, lalu dibagi jumlah token query. + * + * Hasilnya: "Aku Anak Mandiri Watiek" akan secara dinamis menyatukan bobot Judul (0.6) + * dan Pengarang (0.3), BUKAN saling memberikan penalti! + * + * @param string $query String pencarian dari user + * @param \App\Models\Buku $buku Objek buku dari database + * @return float Skor relevansi akhir */ - private function calculateSimilarity($query, $text) + private function calculateDynamicWTS($query, $buku) { - if (empty(trim($text))) return 0; - + // 1. Ekstraksi dan Pembersihan Token Query $queryWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $query))); - $textWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $text))); + $queryWords = array_filter($queryWords); + + if (empty($queryWords)) return 0; - // Frekuensi kata - $vecA = array_count_values(array_filter($queryWords)); - $vecB = array_count_values(array_filter($textWords)); + // 2. Ekstraksi Token Atribut Buku + $judulWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $buku->judul))); + $pengarangWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $buku->pengarang))); + + $teksTambahan = trim(($buku->penerbit ?? '') . ' ' . ($buku->deskripsi ?? '') . ' ' . ($buku->kategori->nama_kategori ?? '')); + $tambahanWords = explode(' ', strtolower(preg_replace('/[^\p{L}\p{N}\s]/u', '', $teksTambahan))); - $terms = array_unique(array_merge(array_keys($vecA), array_keys($vecB))); + // Konversi ke array asosiatif (Hash Map) untuk efisiensi pencarian O(1) + $judulMap = array_flip(array_filter($judulWords)); + $pengarangMap = array_flip(array_filter($pengarangWords)); + $tambahanMap = array_flip(array_filter($tambahanWords)); - $dotProduct = 0; - $normA = 0; - $normB = 0; + // 3. Konfigurasi Bobot WTS + $weights = [ + 'judul' => 0.6, + 'pengarang' => 0.3, + 'tambahan' => 0.1 + ]; - foreach ($terms as $term) { - $valA = $vecA[$term] ?? 0; - $valB = $vecB[$term] ?? 0; + $totalScore = 0; - $dotProduct += ($valA * $valB); - $normA += pow($valA, 2); - $normB += pow($valB, 2); + // 4. Evaluasi Token-wise (Local Max Pooling) + foreach ($queryWords as $qWord) { + // Berikan bobot penuh jika kata ditemukan di atribut bersangkutan + $scoreJudul = isset($judulMap[$qWord]) ? $weights['judul'] : 0; + $scorePengarang = isset($pengarangMap[$qWord]) ? $weights['pengarang'] : 0; + $scoreTambahan = isset($tambahanMap[$qWord]) ? $weights['tambahan'] : 0; + + // KUNCI SOLUSI: Kata ini akan menyumbang skor dari atribut yang paling relevan untuknya + $bestScoreForToken = max($scoreJudul, $scorePengarang, $scoreTambahan); + + $totalScore += $bestScoreForToken; } - if ($normA == 0 || $normB == 0) return 0; - - // Standard Cosine Similarity: $dotProduct / (sqrt($normA) * sqrt($normB)) - // Kelemahannya: Jika judul buku sangat panjang (normB besar), skor akan anjlok (misal < 0.2) - // padahal kata pencariannya cocok 100%. - // Modifikasi menjadi Query Coverage (Pembagi dominan adalah panjang query). - return $dotProduct / $normA; + // 5. Normalisasi + // Membagi total skor dengan jumlah token query agar term-frequency ternormalisasi, + // namun tetap secara matematis mempertahankan keunggulan bobot absolut antar atribut. + return $totalScore / count($queryWords); } public function show($id) diff --git a/get_buku_52.php b/get_buku_52.php new file mode 100644 index 0000000..fa90bff --- /dev/null +++ b/get_buku_52.php @@ -0,0 +1,8 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +$buku = App\Models\Buku::find(52); +echo json_encode($buku->toArray()); diff --git a/get_coordinates.php b/get_coordinates.php new file mode 100644 index 0000000..38913ba --- /dev/null +++ b/get_coordinates.php @@ -0,0 +1,12 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +$res = []; +foreach(['0','1','2','3','4','5','6','7','8','9'] as $d) { + $b = App\Models\Buku::where('nomor_panggil', 'like', $d.'%')->whereNotNull('lokasi_x')->first(); + $res[$d.'00'] = $b ? ['x' => $b->lokasi_x, 'y' => $b->lokasi_y] : null; +} +echo json_encode($res); diff --git a/public/img/img 2.png b/public/img/img 2.png index fd86761..4f33075 100644 Binary files a/public/img/img 2.png and b/public/img/img 2.png differ diff --git a/resources/views/visitor/katalog/show.blade.php b/resources/views/visitor/katalog/show.blade.php index 2f5a406..b0d20f0 100644 --- a/resources/views/visitor/katalog/show.blade.php +++ b/resources/views/visitor/katalog/show.blade.php @@ -283,7 +283,7 @@ class="text-blue-600 font-bold text-sm">{{ $buku->kategori->nama_kategori ?? $lo
- Denah Perpustakaan + Denah Perpustakaan
diff --git a/update_lokasi.php b/update_lokasi.php new file mode 100644 index 0000000..50d5d79 --- /dev/null +++ b/update_lokasi.php @@ -0,0 +1,64 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +$bukus = App\Models\Buku::all(); +$count = 0; + +foreach ($bukus as $buku) { + if (empty($buku->nomor_panggil)) continue; + + $kode_utama = (int) substr(trim(preg_replace('/[^0-9]/', '', $buku->nomor_panggil)), 0, 3); + + $koordinat = match (true) { + $kode_utama >= 0 && $kode_utama <= 99 => match (true) { + $kode_utama <= 19 => ['x' => 13.00, 'y' => 22.00], // Rak 01 + $kode_utama <= 50 => ['x' => 18.00, 'y' => 22.00], // Rak 02 + default => ['x' => 25.00, 'y' => 22.00], // Rak 03-05 + }, + $kode_utama >= 100 && $kode_utama <= 199 => match (true) { + $kode_utama <= 150 => ['x' => 43.00, 'y' => 20.00], // Rak 06-10 + default => ['x' => 57.00, 'y' => 20.00], // Rak 11-14 + }, + $kode_utama >= 200 && $kode_utama <= 299 => match (true) { + $kode_utama == 297 => ['x' => 25.00, 'y' => 38.00], // Rak 25-32 (Islam) + default => ['x' => 13.00, 'y' => 38.00], // Rak 15-24 + }, + $kode_utama >= 300 && $kode_utama <= 399 => match (true) { + $kode_utama <= 330 => ['x' => 43.00, 'y' => 30.00], // Rak 33-36 + $kode_utama <= 360 => ['x' => 50.00, 'y' => 30.00], // Rak 37-40 + default => ['x' => 57.00, 'y' => 30.00], // Rak 41-44 + }, + $kode_utama >= 400 && $kode_utama <= 499 => ['x' => 18.00, 'y' => 62.00], // Rak 45 + $kode_utama >= 500 && $kode_utama <= 599 => ['x' => 50.00, 'y' => 74.00], // Rak 46-48 + $kode_utama >= 600 && $kode_utama <= 699 => match (true) { + $kode_utama <= 610 => ['x' => 35.00, 'y' => 85.00], // Rak 49-53 + $kode_utama <= 630 => ['x' => 45.00, 'y' => 85.00], // Rak 54-58 + $kode_utama <= 650 => ['x' => 55.00, 'y' => 85.00], // Rak 59-63 + default => ['x' => 65.00, 'y' => 85.00], // Rak 64-68 + }, + $kode_utama >= 700 && $kode_utama <= 799 => match (true) { + $kode_utama <= 739 => ['x' => 77.00, 'y' => 22.00], // Rak 71 + $kode_utama <= 769 => ['x' => 82.00, 'y' => 22.00], // Rak 72 + $kode_utama <= 789 => ['x' => 87.00, 'y' => 22.00], // Rak 73 + default => ['x' => 82.00, 'y' => 22.00], // Rak 74 + }, + $kode_utama >= 800 && $kode_utama <= 899 => ['x' => 82.00, 'y' => 32.00], // Rak 77-79 + $kode_utama >= 900 && $kode_utama <= 999 => match (true) { + $kode_utama <= 919 => ['x' => 77.00, 'y' => 42.00], // Rak 69-70 + default => ['x' => 87.00, 'y' => 42.00], // Rak 80-84 + }, + default => ['x' => null, 'y' => null], + }; + + if ($koordinat['x'] !== null) { + $buku->lokasi_x = $koordinat['x']; + $buku->lokasi_y = $koordinat['y']; + $buku->save(); + $count++; + } +} + +echo "Berhasil update $count buku presisi.";