pembaruan sig tps

This commit is contained in:
rahmagustin 2026-02-09 14:43:26 +07:00
parent d54ab1258e
commit ccf66298ab
6 changed files with 457 additions and 434 deletions

View File

@ -10,186 +10,140 @@
class TpsController extends Controller class TpsController extends Controller
{ {
public function index() public function index()
{ {
$title = 'Data TPS'; $title = 'Data TPS';
$tps = LokasiTps::with('kategori')
// Ambil TPS + kategori + jumlah aduan ->withCount('aduan')
$tps = LokasiTps::with('kategori') ->get();
->withCount('aduan') return view('admin.tps.index', compact('title', 'tps'));
->get(); }
return view('admin.tps.index', compact('title', 'tps')); public function create()
} {
$title = 'Tambah TPS';
public function create() $kategori = KategoriTps::all();
{ return view('admin.tps.create', compact('title', 'kategori'));
$title = 'Tambah TPS'; }
$kategori = KategoriTps::all(); private function convertToDecimal($coordinate)
{
return view('admin.tps.create', compact('title', 'kategori')); if (is_numeric($coordinate)) {
} return (float) $coordinate;
}
private function convertToDecimal($coordinate) $coordinate = html_entity_decode($coordinate);
{ $coordinate = strtoupper(trim($coordinate));
// decimal langsung $coordinate = str_replace(
if (is_numeric($coordinate)) { ['°', "'", '"'],
return (float) $coordinate; [' ', ' ', ' '],
} $coordinate
);
$coordinate = html_entity_decode($coordinate); preg_match('/([NSEW])/', $coordinate, $dirMatch);
$coordinate = strtoupper(trim($coordinate)); if (!$dirMatch) return null;
$direction = $dirMatch[1];
// ganti simbol jadi seragam preg_match_all('/\d+(\.\d+)?/', $coordinate, $numbers);
$coordinate = str_replace( if (count($numbers[0]) < 3) return null;
['°', "'", '"'], [$deg, $min, $sec] = array_map('floatval', $numbers[0]);
[' ', ' ', ' '], $decimal = $deg + ($min / 60) + ($sec / 3600);
$coordinate if (in_array($direction, ['S', 'W'])) {
); $decimal *= -1;
}
// ambil arah return $decimal;
preg_match('/([NSEW])/', $coordinate, $dirMatch); }
if (!$dirMatch) return null; public function store(Request $request)
{
$direction = $dirMatch[1]; $request->validate([
'kategori_tps_id' => 'required|exists:kategori_tps,id_kategori_tps',
// ambil angka 'nama_tps' => 'required|string|max:255',
preg_match_all('/\d+(\.\d+)?/', $coordinate, $numbers); 'alamat_tps' => 'required|string|max:255',
if (count($numbers[0]) < 3) return null; 'status_tps' => 'required',
'tahun_pembuatan' => 'required|numeric',
[$deg, $min, $sec] = array_map('floatval', $numbers[0]); 'kapasitas_tps' => 'required',
'latitude' => 'required',
$decimal = $deg + ($min / 60) + ($sec / 3600); 'longitude' => 'required',
'foto_tps' => 'nullable|image|mimes:jpg,jpeg,png|max:2048',
if (in_array($direction, ['S', 'W'])) { ]);
$decimal *= -1; $latitude = $this->convertToDecimal($request->latitude);
} $longitude = $this->convertToDecimal($request->longitude);
if ($latitude === null || $longitude === null) {
return $decimal; return back()->withErrors(['Koordinat tidak valid'])->withInput();
} }
$foto = $request->hasFile('foto_tps')
public function store(Request $request) ? $request->file('foto_tps')->store('foto-tps', 'public')
{ : null;
$request->validate([ LokasiTps::create([
'kategori_tps_id' => 'required|exists:kategori_tps,id_kategori_tps', 'kategori_tps_id' => $request->kategori_tps_id,
'nama_tps' => 'required|string|max:255', 'nama_tps' => $request->nama_tps,
'alamat_tps' => 'required|string|max:255', 'alamat_tps' => $request->alamat_tps,
'status_tps' => 'required', 'status_tps' => $request->status_tps,
'tahun_pembuatan' => 'required|numeric', 'tahun_pembuatan' => $request->tahun_pembuatan,
'kapasitas_tps' => 'required', 'kapasitas_tps' => $request->kapasitas_tps,
'latitude' => 'required', 'latitude' => $latitude,
'longitude' => 'required', 'longitude' => $longitude,
'foto_tps' => 'nullable|image|mimes:jpg,jpeg,png|max:2048', 'foto_tps' => $foto,
]); ]);
return redirect()->route('admin.tps.index')
// 🔥 KONVERSI KOORDINAT ->with('success', 'Data TPS berhasil ditambahkan');
$latitude = $this->convertToDecimal($request->latitude); }
$longitude = $this->convertToDecimal($request->longitude);
public function edit($id)
if ($latitude === null || $longitude === null) { {
return back()->withErrors(['Koordinat tidak valid'])->withInput(); $title = 'Edit TPS';
} $tps = LokasiTps::findOrFail($id);
$kategori = KategoriTps::all();
// Upload foto return view('admin.tps.edit', compact('title', 'tps', 'kategori'));
$foto = $request->hasFile('foto_tps') }
? $request->file('foto_tps')->store('foto-tps', 'public') public function update(Request $request, $id)
: null; {
$tps = LokasiTps::findOrFail($id);
LokasiTps::create([ $request->validate([
'kategori_tps_id' => $request->kategori_tps_id, 'kategori_tps_id' => 'required|exists:kategori_tps,id_kategori_tps',
'nama_tps' => $request->nama_tps, 'nama_tps' => 'required|string|max:255',
'alamat_tps' => $request->alamat_tps, 'alamat_tps' => 'required|string|max:255',
'status_tps' => $request->status_tps, 'status_tps' => 'required',
'tahun_pembuatan' => $request->tahun_pembuatan, 'tahun_pembuatan' => 'required|numeric',
'kapasitas_tps' => $request->kapasitas_tps, 'kapasitas_tps' => 'required',
'latitude' => $latitude, 'latitude' => 'required',
'longitude' => $longitude, 'longitude' => 'required',
'foto_tps' => $foto, 'foto_tps' => 'nullable|image|mimes:jpg,jpeg,png|max:4096',
]); ]);
$latitude = $this->convertToDecimal($request->latitude);
return redirect()->route('admin.tps.index') $longitude = $this->convertToDecimal($request->longitude);
->with('success', 'Data TPS berhasil ditambahkan'); if ($latitude === null || $longitude === null) {
} return back()
->withErrors(['Koordinat tidak valid. Gunakan format Decimal atau DMS.'])
->withInput();
public function edit($id) }
{ if ($request->hasFile('foto_tps')) {
$title = 'Edit TPS'; if ($tps->foto_tps) {
$tps = LokasiTps::findOrFail($id); Storage::disk('public')->delete($tps->foto_tps);
$kategori = KategoriTps::all(); }
$foto = $request->file('foto_tps')->store('foto-tps', 'public');
return view('admin.tps.edit', compact('title', 'tps', 'kategori')); } else {
} $foto = $tps->foto_tps;
}
public function update(Request $request, $id) $tps->update([
{ 'kategori_tps_id' => $request->kategori_tps_id,
$tps = LokasiTps::findOrFail($id); 'nama_tps' => $request->nama_tps,
'alamat_tps' => $request->alamat_tps,
// VALIDASI 'status_tps' => $request->status_tps,
$request->validate([ 'tahun_pembuatan' => $request->tahun_pembuatan,
'kategori_tps_id' => 'required|exists:kategori_tps,id_kategori_tps', 'kapasitas_tps' => $request->kapasitas_tps,
'nama_tps' => 'required|string|max:255', 'latitude' => $latitude,
'alamat_tps' => 'required|string|max:255', 'longitude' => $longitude,
'status_tps' => 'required', 'foto_tps' => $foto,
'tahun_pembuatan' => 'required|numeric', ]);
'kapasitas_tps' => 'required', return redirect()->route('admin.tps.index')
'latitude' => 'required', ->with('success', 'Data TPS berhasil diperbarui');
'longitude' => 'required', }
'foto_tps' => 'nullable|image|mimes:jpg,jpeg,png|max:4096',
]); public function destroy($id)
{
// 🔥 KONVERSI KOORDINAT (DMS / DECIMAL) $tps = LokasiTps::findOrFail($id);
$latitude = $this->convertToDecimal($request->latitude); if ($tps->foto_tps) {
$longitude = $this->convertToDecimal($request->longitude); Storage::disk('public')->delete($tps->foto_tps);
}
// Jika koordinat tidak valid $tps->delete();
if ($latitude === null || $longitude === null) { return redirect()->route('admin.tps.index')
return back() ->with('success', 'Data TPS berhasil dihapus');
->withErrors(['Koordinat tidak valid. Gunakan format Decimal atau DMS.']) }
->withInput();
}
// 📸 UPLOAD FOTO JIKA ADA
if ($request->hasFile('foto_tps')) {
// Hapus foto lama
if ($tps->foto_tps) {
Storage::disk('public')->delete($tps->foto_tps);
}
$foto = $request->file('foto_tps')->store('foto-tps', 'public');
} else {
$foto = $tps->foto_tps;
}
// 💾 UPDATE DATA
$tps->update([
'kategori_tps_id' => $request->kategori_tps_id,
'nama_tps' => $request->nama_tps,
'alamat_tps' => $request->alamat_tps,
'status_tps' => $request->status_tps,
'tahun_pembuatan' => $request->tahun_pembuatan,
'kapasitas_tps' => $request->kapasitas_tps,
'latitude' => $latitude,
'longitude' => $longitude,
'foto_tps' => $foto,
]);
return redirect()->route('admin.tps.index')
->with('success', 'Data TPS berhasil diperbarui');
}
public function destroy($id)
{
$tps = LokasiTps::findOrFail($id);
if ($tps->foto_tps) {
Storage::disk('public')->delete($tps->foto_tps);
}
$tps->delete();
return redirect()->route('admin.tps.index')
->with('success', 'Data TPS berhasil dihapus');
}
} }

View File

@ -8,22 +8,15 @@
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
use App\Models\User;
class LoginRequest extends FormRequest class LoginRequest extends FormRequest
{ {
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool public function authorize(): bool
{ {
return true; return true;
} }
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array public function rules(): array
{ {
return [ return [
@ -32,31 +25,25 @@ public function rules(): array
]; ];
} }
/** public function authenticate(): void
* Attempt to authenticate the request's credentials. {
* $user = User::where('username', $this->username)->first();
* @throws \Illuminate\Validation\ValidationException if (!$user) {
*/ throw ValidationException::withMessages([
public function authenticate(): void 'username' => 'Username tidak terdaftar.',
{ ]);
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('username', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
} }
if (!Auth::attempt([
'username' => $this->username,
'password' => $this->password,
])) {
throw ValidationException::withMessages([
'password' => 'Password yang dimasukkan salah.',
]);
}
}
/**
* Ensure the login request is not rate limited.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function ensureIsNotRateLimited(): void public function ensureIsNotRateLimited(): void
{ {
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
@ -68,18 +55,16 @@ public function ensureIsNotRateLimited(): void
$seconds = RateLimiter::availableIn($this->throttleKey()); $seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([ throw ValidationException::withMessages([
'email' => trans('auth.throttle', [ 'username' => trans('auth.throttle', [
'seconds' => $seconds, 'seconds' => $seconds,
'minutes' => ceil($seconds / 60), 'minutes' => ceil($seconds / 60),
]), ]),
]); ]);
} }
/**
* Get the rate limiting throttle key for the request.
*/
public function throttleKey(): string public function throttleKey(): string
{ {
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); return Str::transliterate(Str::lower($this->string('username')) . '|' . $this->ip());
} }
} }

View File

@ -3027,3 +3027,44 @@ .btn-aduan-tps:hover {
background: #bb2d3b; /* merah lebih gelap */ background: #bb2d3b; /* merah lebih gelap */
color: #fff; color: #fff;
} }
.map-wrapper {
position: relative;
}
.map-action {
position: absolute;
top: 16px;
right: 16px;
z-index: 1000;
background: #fff;
padding: 12px 14px;
border-radius: 12px;
box-shadow: 0 6px 20px rgba(0,0,0,.15);
max-width: 260px;
}
.map-action-text {
font-size: 14px;
color: #333;
margin-bottom: 8px;
line-height: 1.4;
}
.btn-lokasi {
width: 100%;
border: none;
background: #00A86B;
color: #fff;
padding: 8px 12px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
}
.btn-lokasi:hover,
.btn-lokasi:focus:hover {
color: var(--contrast-color);
background: color-mix(in srgb, var(--accent-color), transparent 15%);
}

View File

@ -7,10 +7,10 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="mb-3 d-flex justify-content-between align-items-center">
<div> <div>
<h4 class="card-title mb-0">Data TPS</h4> <h4 class="mb-0 card-title">Data TPS</h4>
<p class="card-description mb-0"> <p class="mb-0 card-description">
Daftar Tempat Pembuangan Sampah (TPS) Daftar Tempat Pembuangan Sampah (TPS)
</p> </p>
</div> </div>
@ -20,82 +20,68 @@
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Nama TPS</th> <th>Nama TPS</th>
<th>Kategori</th> <th>Kategori</th>
<th>Foto</th> <th>Foto</th>
<th>Status</th> <th>Status</th>
<th>Aksi</th> <th>Aksi</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@forelse ($tps as $item)
@forelse ($tps as $item) <tr>
<tr> <td>{{ $item->nama_tps }}</td>
<!-- NAMA --> <td>
<td>{{ $item->nama_tps }}</td> {{ $item->kategori->nama_kategori ?? '-' }}
</td>
<!-- KATEGORI --> <td>
<td> @if ($item->foto_tps)
{{ $item->kategori->nama_kategori ?? '-' }} <img src="{{ asset('storage/' . $item->foto_tps) }}" alt="Foto TPS"
</td> style="
width:200px;
<!-- FOTO --> height:auto;
<td> border-radius:2px;
@if ($item->foto_tps) ">
<img src="{{ asset('storage/' . $item->foto_tps) }}" alt="Foto TPS" @else
style=" <span class="text-muted">-</span>
width:200px; @endif
height:auto; </td>
border-radius:2px; <td>
"> @if ($item->status_tps == 'Aktif')
@else <label class="badge badge-success">Aktif</label>
<span class="text-muted">-</span> @elseif ($item->status_tps == 'Tidak Aktif')
@endif <label class="badge badge-secondary">Tidak Aktif</label>
</td> @else
<label class="badge badge-warning">Pembangunan</label>
@endif
<!-- STATUS --> </td>
<td> <td class="text-center">
@if ($item->status_tps == 'Aktif') <a href="{{ route('admin.tps.edit', $item->id_tps) }}"
<label class="badge badge-success">Aktif</label> class="btn btn-warning btn-sm me-1">
@elseif ($item->status_tps == 'Tidak Aktif') <i class="bi bi-pencil-square"></i>
<label class="badge badge-secondary">Tidak Aktif</label> </a>
@else <form action="{{ route('admin.tps.destroy', $item->id_tps) }}" method="POST"
<label class="badge badge-warning">Pembangunan</label> class="form-hapus" style="display:inline;">
@endif @csrf
</td> @method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">
<!-- AKSI --> <i class="bi bi-trash"></i>
<td class="text-center"> </button>
<a href="{{ route('admin.tps.edit', $item->id_tps) }}" </form>
class="btn btn-warning btn-sm me-1"> </td>
<i class="bi bi-pencil-square"></i> </tr>
</a> @empty
<tr>
<form action="{{ route('admin.tps.destroy', $item->id_tps) }}" method="POST" <td colspan="5" class="text-center">
class="form-hapus" style="display:inline;"> Data TPS belum tersedia
@csrf </td>
@method('DELETE') </tr>
<button type="submit" class="btn btn-danger btn-sm"> @endforelse
<i class="bi bi-trash"></i> </tbody>
</button> </table>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center">
Data TPS belum tersedia
</td>
</tr>
@endforelse
</tbody>
</table>
</div> </div>
</div> </div>

View File

@ -0,0 +1,67 @@
<?php
public function create()
{
$title = 'Tambah Data Sampah';
$users = User::all();
return view('admin.sampah.create', compact('title', 'users'));
}
public function store(Request $request)
{
$request->validate([
'tahun' => 'required|numeric',
'total_sampah' => 'required|numeric',
'total_kelola' => 'required|numeric',
'total_daur_ulang' => 'required|numeric',
]);
$sisa_sampah = $request->total_sampah
- ($request->total_kelola + $request->total_daur_ulang);
Sampah::create([
'user_id' => Auth::id(),
'tahun' => $request->tahun,
'total_sampah' => $request->total_sampah,
'total_kelola' => $request->total_kelola,
'total_daur_ulang' => $request->total_daur_ulang,
'sisa_sampah' => $sisa_sampah,
]);
return redirect()->route('admin.sampah.index')
->with('success', 'Data sampah berhasil ditambahkan');
}
public function edit($id)
{
$title = 'Edit Data Sampah';
$sampah = Sampah::findOrFail($id);
$users = User::all();
return view('admin.sampah.edit', compact('title', 'sampah', 'users'));
}
public function update(Request $request, $id)
{
$sampah = Sampah::findOrFail($id);
$request->validate([
'user_id' => 'required|exists:users,id',
'tahun' => 'required|numeric',
'total_sampah' => 'required|numeric',
'total_kelola' => 'required|numeric',
'total_daur_ulang' => 'required|numeric',
]);
$sisa_sampah = $request->total_sampah
- ($request->total_kelola + $request->total_daur_ulang);
$sampah->update([
'user_id' => $request->user_id,
'tahun' => $request->tahun,
'total_sampah' => $request->total_sampah,
'total_kelola' => $request->total_kelola,
'total_daur_ulang' => $request->total_daur_ulang,
'sisa_sampah' => $sisa_sampah,
]);
return redirect()->route('admin.sampah.index')
->with('success', 'Data sampah berhasil diperbarui');
}
public function destroy($id)
{
$sampah = Sampah::findOrFail($id);
$sampah->delete();
return redirect()->route('admin.sampah.index')
->with('success', 'Data sampah berhasil dihapus');
}

View File

@ -5,7 +5,9 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style> <style>
header, .navbar, #header { header,
.navbar,
#header {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 2000 !important; z-index: 2000 !important;
@ -22,20 +24,7 @@
width: 100%; width: 100%;
height: 450px; height: 450px;
border-radius: 12px; border-radius: 12px;
box-shadow: 0 6px 20px rgba(0,0,0,.12); box-shadow: 0 6px 20px rgba(0, 0, 0, .12);
}
.map-filter {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 12px;
align-items: center;
}
.map-info {
font-size: 14px;
color: #555;
} }
.legend { .legend {
@ -43,7 +32,7 @@
padding: 10px 12px; padding: 10px 12px;
border-radius: 10px; border-radius: 10px;
font-size: 14px; font-size: 14px;
box-shadow: 0 2px 12px rgba(0,0,0,.15); box-shadow: 0 2px 12px rgba(0, 0, 0, .15);
line-height: 18px; line-height: 18px;
} }
@ -53,6 +42,11 @@
float: left; float: left;
margin-right: 8px; margin-right: 8px;
} }
.user-location {
filter: drop-shadow(0 0 8px rgba(13,110,253,0.8))
drop-shadow(0 0 16px rgba(13,110,253,0.6));
}
</style> </style>
<div class="page-title"> <div class="page-title">
@ -69,156 +63,152 @@
<section class="section"> <section class="section">
<div class="container"> <div class="container">
<div class="map-wrapper">
<!-- INFO & BUTTON --> <div class="map-action">
<div class="map-filter"> <div class="map-action-text">
<div class="map-info"> Temukan <b>TPS terdekat</b> dari lokasi Anda saat ini
Klik tombol di samping untuk menemukan <b>TPS terdekat</b> dari lokasi Anda saat ini. </div>
<button type="button" id="btnLokasi" class="btn-lokasi">
Cari TPS Terdekat
</button>
</div> </div>
<button id="btnLokasi" class="btn btn-primary btn-sm"> <div id="mapTPS"></div>
Cari TPS Terdekat
</button>
</div> </div>
<div id="mapTPS"></div>
</div> </div>
</section> </section>
<script> <script>
/* ============================= var map = L.map('mapTPS').setView([-7.6078, 111.903], 12);
INIT MAP
============================= */
var map = L.map('mapTPS').setView([-7.6078, 111.903], 12);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19, maxZoom: 19,
attribution: '© OpenStreetMap' attribution: '© OpenStreetMap'
}).addTo(map); }).addTo(map);
/* ============================= function icon(color) {
ICON TPS return new L.Icon({
============================= */ iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-${color}.png`,
function icon(color) { shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
return new L.Icon({ iconSize: [25, 41],
iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-${color}.png`, iconAnchor: [12, 41],
popupAnchor: [1, -34]
});
}
var iconTPS = icon('green');
var iconTPS3R = icon('blue');
var iconTPA = icon('red');
var iconNear = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-gold.png',
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
iconSize: [25, 41], iconSize: [25, 41],
iconAnchor: [12, 41], iconAnchor: [12, 41],
popupAnchor: [1, -34] popupAnchor: [1, -34]
}); });
}
var iconTPS = icon('green'); var tpsData = @json($tps);
var iconTPS3R = icon('blue'); var markers = [];
var iconTPA = icon('red');
var iconNear = icon('yellow');
/* ============================= tpsData.forEach(tps => {
DATA TPS if (!tps.latitude || !tps.longitude) return;
============================= */
var tpsData = @json($tps);
var markers = [];
/* ============================= let iconUse = iconTPS;
MARKER TPS if (tps.kategori_tps_id == 5) iconUse = iconTPS3R;
============================= */ if (tps.kategori_tps_id == 6) iconUse = iconTPA;
tpsData.forEach(tps => {
if (!tps.latitude || !tps.longitude) return;
let iconUse = iconTPS; let marker = L.marker([tps.latitude, tps.longitude], { icon: iconUse })
if (tps.kategori_tps_id == 5) iconUse = iconTPS3R; .bindPopup(`
if (tps.kategori_tps_id == 6) iconUse = iconTPA; <strong>${tps.nama_tps}</strong><br>
<small>${tps.alamat_tps ?? '-'}</small><br>
<span class="badge bg-success">${tps.status_tps ?? '-'}</span>
<hr style="margin:6px 0">
<a href="/tps/${tps.id_tps}" style="font-size:13px">Detail TPS</a>
`)
.addTo(map);
let marker = L.marker([tps.latitude, tps.longitude], { icon: iconUse }) marker.tpsData = tps;
.bindPopup(` markers.push(marker);
<strong>${tps.nama_tps}</strong><br> });
<small>${tps.alamat_tps ?? '-'}</small><br>
<span class="mt-1 badge bg-success">${tps.status_tps ?? '-'}</span>
<hr style="margin:6px 0">
<a href="/tps/${tps.id_tps}" style="font-size:13px">Detail TPS</a>
`)
.addTo(map);
marker.tpsData = tps; /* ===== LEGEND KATEGORI (BALIK 😤) ===== */
markers.push(marker); var legend = L.control({ position: 'bottomleft' });
}); legend.onAdd = function () {
var div = L.DomUtil.create('div', 'legend');
div.innerHTML = `
<strong>Kategori TPS</strong><br>
<i style="background:#198754"></i> TPS<br>
<i style="background:#0d6efd"></i> TPS 3R<br>
<i style="background:#dc3545"></i> TPA
`;
return div;
};
legend.addTo(map);
/* ============================= function getDistance(lat1, lon1, lat2, lon2) {
LEGEND const R = 6371;
============================= */ const dLat = (lat2 - lat1) * Math.PI / 180;
var legend = L.control({ position: 'bottomleft' }); const dLon = (lon2 - lon1) * Math.PI / 180;
legend.onAdd = function () { const a =
var div = L.DomUtil.create('div', 'legend'); Math.sin(dLat / 2) ** 2 +
div.innerHTML = ` Math.cos(lat1 * Math.PI / 180) *
<strong>Kategori TPS</strong><br> Math.cos(lat2 * Math.PI / 180) *
<i style="background:#198754"></i> TPS<br> Math.sin(dLon / 2) ** 2;
<i style="background:#0d6efd"></i> TPS 3R<br> return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)));
<i style="background:#dc3545"></i> TPA
`;
return div;
};
legend.addTo(map);
/* =============================
HITUNG JARAK
============================= */
function getDistance(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = (lat2-lat1) * Math.PI/180;
const dLon = (lon2-lon1) * Math.PI/180;
const a =
Math.sin(dLat/2)**2 +
Math.cos(lat1*Math.PI/180) *
Math.cos(lat2*Math.PI/180) *
Math.sin(dLon/2)**2;
return R * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));
}
/* =============================
TPS TERDEKAT
============================= */
document.getElementById('btnLokasi').addEventListener('click', function () {
if (!navigator.geolocation) {
alert('Browser tidak mendukung GPS');
return;
} }
navigator.geolocation.getCurrentPosition(pos => { document.getElementById('btnLokasi').addEventListener('click', function () {
let userLat = pos.coords.latitude;
let userLng = pos.coords.longitude;
// LINGKARAN BIRU (USER) if (!navigator.geolocation) {
L.circleMarker([userLat, userLng], { alert('Browser tidak mendukung GPS');
radius: 8, return;
color: '#0d6efd', }
fillColor: '#0d6efd',
fillOpacity: 0.8
}).addTo(map)
.bindPopup('Lokasi Anda')
.openPopup();
let nearest = null; navigator.geolocation.getCurrentPosition(pos => {
let minDist = Infinity; let userLat = pos.coords.latitude;
let userLng = pos.coords.longitude;
markers.forEach(m => { L.circleMarker([userLat, userLng], {
let tps = m.tpsData; radius: 6,
let dist = getDistance(userLat, userLng, tps.latitude, tps.longitude); color: '#0d6efd',
fillColor: '#0d6efd',
fillOpacity: 1,
className: 'user-location'
}).addTo(map)
.bindPopup('Lokasi Anda')
.openPopup();
if (dist < minDist) { setInterval(() => {
minDist = dist; let pulse = L.circle([userLat, userLng], {
nearest = m; radius: 40,
nearest.distance = dist; color: '#0d6efd',
fillColor: '#0d6efd',
fillOpacity: 0.35,
weight: 0
}).addTo(map);
setTimeout(() => map.removeLayer(pulse), 1000);
}, 1200);
let nearest = null;
let minDist = Infinity;
markers.forEach(m => {
let tps = m.tpsData;
let dist = getDistance(userLat, userLng, tps.latitude, tps.longitude);
if (dist < minDist) {
minDist = dist;
nearest = m;
}
});
if (nearest) {
nearest.setIcon(iconNear);
nearest.openPopup();
map.setView(nearest.getLatLng(), 15);
} }
}); });
if (nearest) {
nearest.setIcon(iconNear);
nearest.openPopup();
map.setView(nearest.getLatLng(), 15);
}
}); });
});
</script> </script>
@endsection @endsection