feat: leaflet
parent
80c4c8e758
commit
29f1c5fb66
|
|
@ -1,7 +1,12 @@
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Profil Bank Sampah";
|
ViewData["Title"] = "Profil Bank Sampah";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@section Styles {
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="~/css/leaflet-map-picker.css" />
|
||||||
|
}
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:gap-0">
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:gap-0">
|
||||||
<div class="prose">
|
<div class="prose">
|
||||||
<span class="text-xl font-semibold text-black">
|
<span class="text-xl font-semibold text-black">
|
||||||
|
|
@ -274,7 +279,7 @@
|
||||||
<legend class="fieldset-legend">
|
<legend class="fieldset-legend">
|
||||||
Latitude
|
Latitude
|
||||||
</legend>
|
</legend>
|
||||||
<input type="number" class="input" placeholder="Latitude" value="324242" />
|
<input type="number" step="any" class="input" id="latitude-input" name="latitude" placeholder="Latitude" value="-6.2088" />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
|
@ -282,10 +287,24 @@
|
||||||
<legend class="fieldset-legend">
|
<legend class="fieldset-legend">
|
||||||
Longitude
|
Longitude
|
||||||
</legend>
|
</legend>
|
||||||
<input type="number" class="input" placeholder="Longitude" value="-371872" />
|
<input type="number" step="any" class="input" id="longitude-input" name="longitude" placeholder="Longitude" value="106.8456" />
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Picker -->
|
||||||
|
<div class="mt-6">
|
||||||
|
@{
|
||||||
|
ViewData["MapId"] = "map-picker";
|
||||||
|
ViewData["LatInputId"] = "latitude-input";
|
||||||
|
ViewData["LngInputId"] = "longitude-input";
|
||||||
|
ViewData["SearchInputId"] = "search-location";
|
||||||
|
ViewData["SearchResultsId"] = "search-results";
|
||||||
|
ViewData["ClearSearchBtnId"] = "clear-search";
|
||||||
|
ViewData["Label"] = "Pilih Lokasi di Peta";
|
||||||
|
}
|
||||||
|
@await Html.PartialAsync("_LeafletMapPicker")
|
||||||
|
</div>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<div class="grid grid-cols-1 space-y-2 md:grid-cols-4 md:space-x-6">
|
<div class="grid grid-cols-1 space-y-2 md:grid-cols-4 md:space-x-6">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
|
@ -321,3 +340,23 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="~/js/leaflet-map-picker.js"></script>
|
||||||
|
<script>
|
||||||
|
// Initialize the reusable Leaflet Map Picker component
|
||||||
|
window.leafletMapPickerInstance = new LeafletMapPicker({
|
||||||
|
mapElementId: 'map-picker',
|
||||||
|
latInputId: 'latitude-input',
|
||||||
|
lngInputId: 'longitude-input',
|
||||||
|
searchInputId: 'search-location',
|
||||||
|
searchResultsId: 'search-results',
|
||||||
|
clearSearchBtnId: 'clear-search',
|
||||||
|
alamatInputSelector: 'textarea[name="alamat_lengkap"]',
|
||||||
|
initialLat: -6.2088,
|
||||||
|
initialLng: 106.8456,
|
||||||
|
initialZoom: 12
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,17 @@
|
||||||
ViewData["Title"] = "Profil Bank Sampah";
|
ViewData["Title"] = "Profil Bank Sampah";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@section Styles {
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<style>
|
||||||
|
#map-display {
|
||||||
|
height: 400px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:gap-0">
|
<div class="flex flex-col gap-2 md:flex-row md:justify-between md:gap-0">
|
||||||
<div class="prose">
|
<div class="prose">
|
||||||
<span class="text-xl font-semibold text-black">
|
<span class="text-xl font-semibold text-black">
|
||||||
|
|
@ -149,12 +160,55 @@
|
||||||
<div class="grid grid-cols-1 space-y-2 md:grid-cols-4">
|
<div class="grid grid-cols-1 space-y-2 md:grid-cols-4">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-xs text-gray-500">Latitude</span>
|
<span class="text-xs text-gray-500">Latitude</span>
|
||||||
<span class="text-sm">324242</span>
|
<span class="text-sm" id="latitude-value">-6.2088</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-xs text-gray-500">Longitude</span>
|
<span class="text-xs text-gray-500">Longitude</span>
|
||||||
<span class="text-sm">-371872</span>
|
<span class="text-sm" id="longitude-value">106.8456</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Map Display -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-700 mb-3">
|
||||||
|
<i class="ph ph-map-pin me-2"></i>
|
||||||
|
Lokasi di Peta
|
||||||
|
</h3>
|
||||||
|
<div id="map-display" class="rounded-lg border border-gray-300"></div>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
<i class="ph ph-info me-1"></i>
|
||||||
|
Peta menampilkan lokasi Bank Sampah
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script>
|
||||||
|
// Get coordinates from data
|
||||||
|
const lat = parseFloat(document.getElementById('latitude-value').textContent);
|
||||||
|
const lng = parseFloat(document.getElementById('longitude-value').textContent);
|
||||||
|
|
||||||
|
// Initialize map
|
||||||
|
const mapDisplay = L.map('map-display').setView([lat, lng], 15);
|
||||||
|
|
||||||
|
// Add tile layer
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
maxZoom: 18,
|
||||||
|
minZoom: 4
|
||||||
|
}).addTo(mapDisplay);
|
||||||
|
|
||||||
|
// Add marker (non-draggable for view mode)
|
||||||
|
const marker = L.marker([lat, lng]).addTo(mapDisplay);
|
||||||
|
marker.bindPopup(`
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<strong>Lokasi Bank Sampah</strong><br>
|
||||||
|
<small>${lat.toFixed(8)}, ${lng.toFixed(8)}</small>
|
||||||
|
</div>
|
||||||
|
`).openPopup();
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
@*
|
||||||
|
Leaflet Map Picker - Reusable Partial View
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@await Html.PartialAsync("_LeafletMapPicker", new LeafletMapPickerModel
|
||||||
|
{
|
||||||
|
MapId = "map-picker",
|
||||||
|
LatInputId = "latitude-input",
|
||||||
|
LngInputId = "longitude-input",
|
||||||
|
SearchInputId = "search-location",
|
||||||
|
SearchResultsId = "search-results",
|
||||||
|
ClearSearchBtnId = "clear-search",
|
||||||
|
Label = "Pilih Lokasi di Peta",
|
||||||
|
HelpText = "Cari lokasi menggunakan search box di atas, atau klik pada peta untuk menentukan lokasi secara manual"
|
||||||
|
})
|
||||||
|
*@
|
||||||
|
|
||||||
|
@model dynamic
|
||||||
|
|
||||||
|
@{
|
||||||
|
var mapId = ViewData["MapId"]?.ToString() ?? "map-picker";
|
||||||
|
var latInputId = ViewData["LatInputId"]?.ToString() ?? "latitude-input";
|
||||||
|
var lngInputId = ViewData["LngInputId"]?.ToString() ?? "longitude-input";
|
||||||
|
var searchInputId = ViewData["SearchInputId"]?.ToString() ?? "search-location";
|
||||||
|
var searchResultsId = ViewData["SearchResultsId"]?.ToString() ?? "search-results";
|
||||||
|
var clearSearchBtnId = ViewData["ClearSearchBtnId"]?.ToString() ?? "clear-search";
|
||||||
|
var label = ViewData["Label"]?.ToString() ?? "Pilih Lokasi di Peta";
|
||||||
|
var helpText = ViewData["HelpText"]?.ToString() ?? "Cari lokasi menggunakan search box di atas, atau klik pada peta untuk menentukan lokasi secara manual";
|
||||||
|
var showLabel = ViewData["ShowLabel"] == null ? true : (ViewData["ShowLabel"] is bool labelValue && labelValue);
|
||||||
|
var showSearch = ViewData["ShowSearch"] == null ? true : (ViewData["ShowSearch"] is bool searchValue && searchValue);
|
||||||
|
var showHelpText = ViewData["ShowHelpText"] == null ? true : (ViewData["ShowHelpText"] is bool helpValue && helpValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="leaflet-map-picker-container">
|
||||||
|
@if (showLabel)
|
||||||
|
{
|
||||||
|
<label class="text-sm font-semibold text-gray-700 mb-2 block">
|
||||||
|
<i class="ph ph-map-pin me-2"></i>
|
||||||
|
@label
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (showSearch)
|
||||||
|
{
|
||||||
|
<!-- Search Box -->
|
||||||
|
<div class="leaflet-map-picker-search">
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="@searchInputId"
|
||||||
|
class="input leaflet-map-picker-search-input"
|
||||||
|
placeholder="Cari lokasi (nama tempat, alamat, kota)..."
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<i class="ph ph-magnifying-glass leaflet-map-picker-search-icon"></i>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="@clearSearchBtnId"
|
||||||
|
class="leaflet-map-picker-clear-btn hidden"
|
||||||
|
>
|
||||||
|
<i class="ph ph-x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<!-- Search Results Dropdown -->
|
||||||
|
<div id="@searchResultsId" class="leaflet-map-picker-results hidden"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div id="@mapId" class="leaflet-map-picker"></div>
|
||||||
|
|
||||||
|
@if (showHelpText)
|
||||||
|
{
|
||||||
|
<p class="leaflet-map-picker-help-text">
|
||||||
|
<i class="ph ph-info me-1"></i>
|
||||||
|
@helpText
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,107 @@
|
||||||
|
/**
|
||||||
|
* Leaflet Map Picker - Reusable Styles
|
||||||
|
*/
|
||||||
|
|
||||||
|
.leaflet-map-picker-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker {
|
||||||
|
height: 400px;
|
||||||
|
width: 100%;
|
||||||
|
cursor: crosshair;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-search {
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.75rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-clear-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #9ca3af;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-clear-btn:hover {
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-results {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||||
|
max-height: 16rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-results.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-result-item {
|
||||||
|
padding: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-result-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-result-item:hover {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-map-picker-help-text {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #6b7280;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner */
|
||||||
|
.leaflet-map-picker-spinner {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
border-top-color: transparent;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: leaflet-map-picker-spin 0.6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes leaflet-map-picker-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.leaflet-map-picker {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,309 @@
|
||||||
|
# Leaflet Map Picker - Dokumentasi
|
||||||
|
|
||||||
|
Komponen reusable untuk memilih lokasi menggunakan Leaflet Maps dengan fitur pencarian dan reverse geocoding.
|
||||||
|
|
||||||
|
## Fitur
|
||||||
|
|
||||||
|
- 🗺️ Interactive map dengan OpenStreetMap
|
||||||
|
- 🔍 Pencarian lokasi dengan Nominatim API
|
||||||
|
- 📍 Click-to-select koordinat
|
||||||
|
- 🏠 Reverse geocoding untuk mendapatkan alamat otomatis
|
||||||
|
- 📱 Responsive design
|
||||||
|
- ⚡ Mudah dikonfigurasi dan reusable
|
||||||
|
|
||||||
|
## Instalasi
|
||||||
|
|
||||||
|
### 1. Include CSS dan JavaScript
|
||||||
|
|
||||||
|
Tambahkan di section `Styles` dan `Scripts` pada view Anda:
|
||||||
|
|
||||||
|
```cshtml
|
||||||
|
@section Styles {
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="~/css/leaflet-map-picker.css" />
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="~/js/leaflet-map-picker.js"></script>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Penggunaan
|
||||||
|
|
||||||
|
### Metode 1: Menggunakan Partial View (Recommended)
|
||||||
|
|
||||||
|
#### Langkah 1: Tambahkan Input Fields untuk Latitude dan Longitude
|
||||||
|
|
||||||
|
```cshtml
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Latitude</legend>
|
||||||
|
<input type="number" step="any" class="input" id="latitude-input" name="latitude" placeholder="Latitude" />
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Longitude</legend>
|
||||||
|
<input type="number" step="any" class="input" id="longitude-input" name="longitude" placeholder="Longitude" />
|
||||||
|
</fieldset>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Langkah 2: Include Partial View
|
||||||
|
|
||||||
|
```cshtml
|
||||||
|
<div class="mt-6">
|
||||||
|
@{
|
||||||
|
ViewData["MapId"] = "map-picker";
|
||||||
|
ViewData["LatInputId"] = "latitude-input";
|
||||||
|
ViewData["LngInputId"] = "longitude-input";
|
||||||
|
ViewData["SearchInputId"] = "search-location";
|
||||||
|
ViewData["SearchResultsId"] = "search-results";
|
||||||
|
ViewData["ClearSearchBtnId"] = "clear-search";
|
||||||
|
ViewData["Label"] = "Pilih Lokasi di Peta";
|
||||||
|
}
|
||||||
|
@await Html.PartialAsync("_LeafletMapPicker")
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Langkah 3: Initialize JavaScript Component
|
||||||
|
|
||||||
|
```cshtml
|
||||||
|
@section Scripts {
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="~/js/leaflet-map-picker.js"></script>
|
||||||
|
<script>
|
||||||
|
window.leafletMapPickerInstance = new LeafletMapPicker({
|
||||||
|
mapElementId: 'map-picker',
|
||||||
|
latInputId: 'latitude-input',
|
||||||
|
lngInputId: 'longitude-input',
|
||||||
|
searchInputId: 'search-location',
|
||||||
|
searchResultsId: 'search-results',
|
||||||
|
clearSearchBtnId: 'clear-search',
|
||||||
|
alamatInputSelector: 'textarea[name="alamat_lengkap"]', // Optional
|
||||||
|
initialLat: -6.2088,
|
||||||
|
initialLng: 106.8456,
|
||||||
|
initialZoom: 12
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Metode 2: HTML Manual
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="leaflet-map-picker-container">
|
||||||
|
<label class="text-sm font-semibold text-gray-700 mb-2 block">
|
||||||
|
<i class="ph ph-map-pin me-2"></i>
|
||||||
|
Pilih Lokasi di Peta
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Search Box -->
|
||||||
|
<div class="leaflet-map-picker-search">
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="search-location"
|
||||||
|
class="input leaflet-map-picker-search-input"
|
||||||
|
placeholder="Cari lokasi (nama tempat, alamat, kota)..."
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<i class="ph ph-magnifying-glass leaflet-map-picker-search-icon"></i>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="clear-search"
|
||||||
|
class="leaflet-map-picker-clear-btn hidden"
|
||||||
|
>
|
||||||
|
<i class="ph ph-x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="search-results" class="leaflet-map-picker-results hidden"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map-picker" class="leaflet-map-picker"></div>
|
||||||
|
<p class="leaflet-map-picker-help-text">
|
||||||
|
<i class="ph ph-info me-1"></i>
|
||||||
|
Cari lokasi menggunakan search box di atas, atau klik pada peta untuk menentukan lokasi secara manual
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfigurasi Options
|
||||||
|
|
||||||
|
| Option | Type | Default | Deskripsi |
|
||||||
|
|--------|------|---------|-----------|
|
||||||
|
| `mapElementId` | string | `'map-picker'` | ID elemen untuk map container |
|
||||||
|
| `latInputId` | string | `'latitude-input'` | ID input field untuk latitude |
|
||||||
|
| `lngInputId` | string | `'longitude-input'` | ID input field untuk longitude |
|
||||||
|
| `searchInputId` | string | `'search-location'` | ID input field untuk search box |
|
||||||
|
| `searchResultsId` | string | `'search-results'` | ID elemen untuk search results dropdown |
|
||||||
|
| `clearSearchBtnId` | string | `'clear-search'` | ID button untuk clear search |
|
||||||
|
| `alamatInputSelector` | string | `null` | CSS selector untuk input alamat (optional) |
|
||||||
|
| `initialLat` | number | `-6.2088` | Latitude awal map (Jakarta) |
|
||||||
|
| `initialLng` | number | `106.8456` | Longitude awal map (Jakarta) |
|
||||||
|
| `initialZoom` | number | `12` | Zoom level awal |
|
||||||
|
| `maxZoom` | number | `18` | Maximum zoom level |
|
||||||
|
| `minZoom` | number | `4` | Minimum zoom level |
|
||||||
|
| `searchDebounceMs` | number | `500` | Debounce delay untuk search (ms) |
|
||||||
|
| `countryCode` | string | `'id'` | Country code untuk filter pencarian |
|
||||||
|
| `language` | string | `'id'` | Language untuk API results |
|
||||||
|
|
||||||
|
## ViewData Options untuk Partial View
|
||||||
|
|
||||||
|
| Key | Type | Default | Deskripsi |
|
||||||
|
|-----|------|---------|-----------|
|
||||||
|
| `MapId` | string | `'map-picker'` | ID untuk map element |
|
||||||
|
| `LatInputId` | string | `'latitude-input'` | ID untuk latitude input |
|
||||||
|
| `LngInputId` | string | `'longitude-input'` | ID untuk longitude input |
|
||||||
|
| `SearchInputId` | string | `'search-location'` | ID untuk search input |
|
||||||
|
| `SearchResultsId` | string | `'search-results'` | ID untuk search results |
|
||||||
|
| `ClearSearchBtnId` | string | `'clear-search'` | ID untuk clear button |
|
||||||
|
| `Label` | string | `'Pilih Lokasi di Peta'` | Label text |
|
||||||
|
| `HelpText` | string | Default help text | Help text di bawah map |
|
||||||
|
| `ShowLabel` | bool | `true` | Tampilkan label |
|
||||||
|
| `ShowSearch` | bool | `true` | Tampilkan search box |
|
||||||
|
| `ShowHelpText` | bool | `true` | Tampilkan help text |
|
||||||
|
|
||||||
|
## Public API Methods
|
||||||
|
|
||||||
|
### `getCoordinates()`
|
||||||
|
Mendapatkan koordinat saat ini.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const coords = leafletMapPickerInstance.getCoordinates();
|
||||||
|
console.log(coords); // { lat: -6.2088, lng: 106.8456 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### `setCoordinates(lat, lng, updateMarker = true)`
|
||||||
|
Set koordinat secara programmatic.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
leafletMapPickerInstance.setCoordinates(-6.2088, 106.8456, true);
|
||||||
|
```
|
||||||
|
|
||||||
|
### `destroy()`
|
||||||
|
Clean up instance dan event listeners.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
leafletMapPickerInstance.destroy();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contoh Lengkap
|
||||||
|
|
||||||
|
```cshtml
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Edit Lokasi";
|
||||||
|
}
|
||||||
|
|
||||||
|
@section Styles {
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="~/css/leaflet-map-picker.css" />
|
||||||
|
}
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Latitude</legend>
|
||||||
|
<input type="number" step="any" class="input" id="latitude-input" name="latitude" value="-6.2088" />
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Longitude</legend>
|
||||||
|
<input type="number" step="any" class="input" id="longitude-input" name="longitude" value="106.8456" />
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend class="fieldset-legend">Alamat</legend>
|
||||||
|
<textarea class="textarea" name="alamat_lengkap" placeholder="Alamat lengkap"></textarea>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewData["MapId"] = "map-picker";
|
||||||
|
ViewData["LatInputId"] = "latitude-input";
|
||||||
|
ViewData["LngInputId"] = "longitude-input";
|
||||||
|
ViewData["SearchInputId"] = "search-location";
|
||||||
|
ViewData["SearchResultsId"] = "search-results";
|
||||||
|
ViewData["ClearSearchBtnId"] = "clear-search";
|
||||||
|
ViewData["Label"] = "Pilih Lokasi di Peta";
|
||||||
|
}
|
||||||
|
@await Html.PartialAsync("_LeafletMapPicker")
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary">Simpan</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="~/js/leaflet-map-picker.js"></script>
|
||||||
|
<script>
|
||||||
|
window.leafletMapPickerInstance = new LeafletMapPicker({
|
||||||
|
mapElementId: 'map-picker',
|
||||||
|
latInputId: 'latitude-input',
|
||||||
|
lngInputId: 'longitude-input',
|
||||||
|
searchInputId: 'search-location',
|
||||||
|
searchResultsId: 'search-results',
|
||||||
|
clearSearchBtnId: 'clear-search',
|
||||||
|
alamatInputSelector: 'textarea[name="alamat_lengkap"]',
|
||||||
|
initialLat: -6.2088,
|
||||||
|
initialLng: 106.8456,
|
||||||
|
initialZoom: 12
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Styling
|
||||||
|
|
||||||
|
Anda bisa override CSS classes untuk custom styling:
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Custom map height */
|
||||||
|
.leaflet-map-picker {
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom search box styling */
|
||||||
|
.leaflet-map-picker-search-input {
|
||||||
|
border-color: #your-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom search results */
|
||||||
|
.leaflet-map-picker-result-item:hover {
|
||||||
|
background-color: #your-color;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
|
||||||
|
- Chrome (latest)
|
||||||
|
- Firefox (latest)
|
||||||
|
- Safari (latest)
|
||||||
|
- Edge (latest)
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Leaflet 1.9.4+
|
||||||
|
- OpenStreetMap (Tile Layer)
|
||||||
|
- Nominatim API (Geocoding)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Map tidak muncul
|
||||||
|
- Pastikan Leaflet CSS dan JS sudah ter-load
|
||||||
|
- Periksa console untuk error
|
||||||
|
- Pastikan element ID sudah benar
|
||||||
|
|
||||||
|
### Search tidak bekerja
|
||||||
|
- Pastikan ada koneksi internet
|
||||||
|
- Nominatim API memiliki rate limit
|
||||||
|
- Periksa browser console untuk error
|
||||||
|
|
||||||
|
### Koordinat tidak update
|
||||||
|
- Pastikan input ID sudah benar
|
||||||
|
- Periksa attribute `step="any"` pada input number
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Bank Sampah Development Team
|
||||||
|
|
@ -0,0 +1,423 @@
|
||||||
|
/**
|
||||||
|
* Leaflet Map Picker - Reusable Component
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* const mapPicker = new LeafletMapPicker({
|
||||||
|
* mapElementId: 'map-picker',
|
||||||
|
* latInputId: 'latitude-input',
|
||||||
|
* lngInputId: 'longitude-input',
|
||||||
|
* searchInputId: 'search-location',
|
||||||
|
* searchResultsId: 'search-results',
|
||||||
|
* clearSearchBtnId: 'clear-search',
|
||||||
|
* alamatInputSelector: 'textarea[name="alamat_lengkap"]',
|
||||||
|
* initialLat: -6.2088,
|
||||||
|
* initialLng: 106.8456,
|
||||||
|
* initialZoom: 12
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
|
||||||
|
class LeafletMapPicker {
|
||||||
|
constructor(options) {
|
||||||
|
// Default options
|
||||||
|
this.options = {
|
||||||
|
mapElementId: 'map-picker',
|
||||||
|
latInputId: 'latitude-input',
|
||||||
|
lngInputId: 'longitude-input',
|
||||||
|
searchInputId: 'search-location',
|
||||||
|
searchResultsId: 'search-results',
|
||||||
|
clearSearchBtnId: 'clear-search',
|
||||||
|
alamatInputSelector: null,
|
||||||
|
initialLat: -6.2088,
|
||||||
|
initialLng: 106.8456,
|
||||||
|
initialZoom: 12,
|
||||||
|
maxZoom: 18,
|
||||||
|
minZoom: 4,
|
||||||
|
searchDebounceMs: 500,
|
||||||
|
countryCode: 'id',
|
||||||
|
language: 'id',
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
// State
|
||||||
|
this.currentMarker = null;
|
||||||
|
this.searchTimeout = null;
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// Get DOM elements
|
||||||
|
this.mapElement = document.getElementById(this.options.mapElementId);
|
||||||
|
this.latInput = document.getElementById(this.options.latInputId);
|
||||||
|
this.lngInput = document.getElementById(this.options.lngInputId);
|
||||||
|
this.searchInput = document.getElementById(this.options.searchInputId);
|
||||||
|
this.searchResults = document.getElementById(this.options.searchResultsId);
|
||||||
|
this.clearSearchBtn = document.getElementById(this.options.clearSearchBtnId);
|
||||||
|
this.alamatInput = this.options.alamatInputSelector
|
||||||
|
? document.querySelector(this.options.alamatInputSelector)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!this.mapElement) {
|
||||||
|
console.error(`Map element with id "${this.options.mapElementId}" not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize map
|
||||||
|
this.initMap();
|
||||||
|
|
||||||
|
// Bind events
|
||||||
|
this.bindEvents();
|
||||||
|
|
||||||
|
// Initialize marker if coordinates already exist
|
||||||
|
this.initializeExistingMarker();
|
||||||
|
}
|
||||||
|
|
||||||
|
initMap() {
|
||||||
|
// Initialize map
|
||||||
|
this.map = L.map(this.mapElement).setView(
|
||||||
|
[this.options.initialLat, this.options.initialLng],
|
||||||
|
this.options.initialZoom
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add tile layer
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors',
|
||||||
|
maxZoom: this.options.maxZoom,
|
||||||
|
minZoom: this.options.minZoom
|
||||||
|
}).addTo(this.map);
|
||||||
|
|
||||||
|
// Handle map click
|
||||||
|
this.map.on('click', (e) => this.onMapClick(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Search input events
|
||||||
|
if (this.searchInput) {
|
||||||
|
this.searchInput.addEventListener('input', (e) => this.onSearchInput(e));
|
||||||
|
this.searchInput.addEventListener('keydown', (e) => this.onSearchKeydown(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear search button
|
||||||
|
if (this.clearSearchBtn) {
|
||||||
|
this.clearSearchBtn.addEventListener('click', () => this.clearSearch());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual coordinate input events
|
||||||
|
if (this.latInput) {
|
||||||
|
this.latInput.addEventListener('change', () => this.onCoordinateChange());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.lngInput) {
|
||||||
|
this.lngInput.addEventListener('change', () => this.onCoordinateChange());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close search results when clicking outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (this.searchInput && this.searchResults &&
|
||||||
|
!this.searchInput.contains(e.target) &&
|
||||||
|
!this.searchResults.contains(e.target)) {
|
||||||
|
this.searchResults.classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeExistingMarker() {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const lat = parseFloat(this.latInput?.value);
|
||||||
|
const lng = parseFloat(this.lngInput?.value);
|
||||||
|
|
||||||
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||||||
|
this.updateMarker(lat, lng);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onMapClick(e) {
|
||||||
|
const lat = e.latlng.lat;
|
||||||
|
const lng = e.latlng.lng;
|
||||||
|
|
||||||
|
// Update form inputs
|
||||||
|
if (this.latInput) this.latInput.value = lat.toFixed(8);
|
||||||
|
if (this.lngInput) this.lngInput.value = lng.toFixed(8);
|
||||||
|
|
||||||
|
// Update marker and fetch address
|
||||||
|
this.updateMarker(lat, lng, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchInput(e) {
|
||||||
|
const query = e.target.value.trim();
|
||||||
|
|
||||||
|
// Show/hide clear button
|
||||||
|
if (this.clearSearchBtn) {
|
||||||
|
if (query.length > 0) {
|
||||||
|
this.clearSearchBtn.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
this.clearSearchBtn.classList.add('hidden');
|
||||||
|
if (this.searchResults) {
|
||||||
|
this.searchResults.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debounce search
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
this.searchLocation(query);
|
||||||
|
}, this.options.searchDebounceMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchKeydown(e) {
|
||||||
|
// Prevent form submission on Enter
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCoordinateChange() {
|
||||||
|
const lat = parseFloat(this.latInput?.value);
|
||||||
|
const lng = parseFloat(this.lngInput?.value);
|
||||||
|
|
||||||
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||||||
|
this.updateMarker(lat, lng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchLocation(query) {
|
||||||
|
if (!this.searchResults || query.length < 3) {
|
||||||
|
if (this.searchResults) {
|
||||||
|
this.searchResults.classList.add('hidden');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
this.searchResults.innerHTML = `
|
||||||
|
<div class="p-4 text-center">
|
||||||
|
<div class="inline-flex items-center gap-3">
|
||||||
|
<div class="w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin"></div>
|
||||||
|
<span class="text-gray-600">Mencari lokasi...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.searchResults.classList.remove('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&countrycodes=${this.options.countryCode}&limit=5&addressdetails=1`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': this.options.language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
this.displaySearchResults(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching location:', error);
|
||||||
|
this.searchResults.innerHTML = `
|
||||||
|
<div class="p-4 text-center text-red-600">
|
||||||
|
<i class="ph ph-warning me-2"></i>
|
||||||
|
Gagal mencari lokasi. Silakan coba lagi.
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
displaySearchResults(results) {
|
||||||
|
if (!this.searchResults) return;
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
this.searchResults.innerHTML = `
|
||||||
|
<div class="p-4 text-center text-gray-500">
|
||||||
|
<i class="ph ph-info me-2"></i>
|
||||||
|
Lokasi tidak ditemukan
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
this.searchResults.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.searchResults.innerHTML = results.map(result => {
|
||||||
|
const escapedDisplayName = result.display_name.replace(/'/g, "\\'");
|
||||||
|
return `
|
||||||
|
<div class="p-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-b-0 transition-colors"
|
||||||
|
onclick="window.leafletMapPickerInstance.selectSearchResult(${result.lat}, ${result.lon}, '${escapedDisplayName}')">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<i class="ph ph-map-pin text-primary mt-1"></i>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="text-sm font-medium text-gray-800 truncate">${result.display_name}</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">
|
||||||
|
${parseFloat(result.lat).toFixed(6)}, ${parseFloat(result.lon).toFixed(6)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
this.searchResults.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
selectSearchResult(lat, lon, displayName) {
|
||||||
|
// Update coordinates
|
||||||
|
if (this.latInput) this.latInput.value = parseFloat(lat).toFixed(8);
|
||||||
|
if (this.lngInput) this.lngInput.value = parseFloat(lon).toFixed(8);
|
||||||
|
|
||||||
|
// Update marker
|
||||||
|
this.updateMarker(lat, lon, true);
|
||||||
|
|
||||||
|
// Clear search
|
||||||
|
if (this.searchResults) this.searchResults.classList.add('hidden');
|
||||||
|
if (this.searchInput) this.searchInput.value = '';
|
||||||
|
if (this.clearSearchBtn) this.clearSearchBtn.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSearch() {
|
||||||
|
if (this.searchInput) this.searchInput.value = '';
|
||||||
|
if (this.searchResults) this.searchResults.classList.add('hidden');
|
||||||
|
if (this.clearSearchBtn) this.clearSearchBtn.classList.add('hidden');
|
||||||
|
if (this.searchInput) this.searchInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchAddress(lat, lng) {
|
||||||
|
if (!this.alamatInput) return;
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
const originalPlaceholder = this.alamatInput.placeholder;
|
||||||
|
this.alamatInput.placeholder = 'Mengambil alamat dari koordinat...';
|
||||||
|
this.alamatInput.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': this.options.language
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data && data.display_name) {
|
||||||
|
// Build formatted address
|
||||||
|
const addr = data.address || {};
|
||||||
|
let formattedAddress = '';
|
||||||
|
|
||||||
|
// Try to build a structured address
|
||||||
|
if (addr.road || addr.suburb || addr.village) {
|
||||||
|
const parts = [];
|
||||||
|
if (addr.road) parts.push(addr.road);
|
||||||
|
if (addr.house_number) parts.push('No. ' + addr.house_number);
|
||||||
|
if (addr.suburb || addr.village) parts.push(addr.suburb || addr.village);
|
||||||
|
if (addr.city || addr.town || addr.city_district) {
|
||||||
|
parts.push(addr.city || addr.town || addr.city_district);
|
||||||
|
}
|
||||||
|
if (addr.state) parts.push(addr.state);
|
||||||
|
if (addr.postcode) parts.push(addr.postcode);
|
||||||
|
|
||||||
|
formattedAddress = parts.join(', ');
|
||||||
|
} else {
|
||||||
|
// Fallback to display_name
|
||||||
|
formattedAddress = data.display_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.alamatInput.value = formattedAddress;
|
||||||
|
this.alamatInput.placeholder = originalPlaceholder;
|
||||||
|
|
||||||
|
// Show success feedback
|
||||||
|
this.alamatInput.classList.add('border-success');
|
||||||
|
setTimeout(() => this.alamatInput.classList.remove('border-success'), 2000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching address:', error);
|
||||||
|
this.alamatInput.placeholder = 'Gagal mengambil alamat, silakan isi manual...';
|
||||||
|
setTimeout(() => this.alamatInput.placeholder = originalPlaceholder, 3000);
|
||||||
|
} finally {
|
||||||
|
this.alamatInput.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMarker(lat, lng, fetchAddr = false) {
|
||||||
|
// Remove existing marker
|
||||||
|
if (this.currentMarker) {
|
||||||
|
this.map.removeLayer(this.currentMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new draggable marker
|
||||||
|
this.currentMarker = L.marker([lat, lng], {
|
||||||
|
draggable: true
|
||||||
|
}).addTo(this.map);
|
||||||
|
|
||||||
|
this.currentMarker.bindPopup(`
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<strong>Lokasi Dipilih</strong><br>
|
||||||
|
<small>${lat.toFixed(8)}, ${lng.toFixed(8)}</small><br>
|
||||||
|
<small style="color: #6b7280; margin-top: 4px; display: block;">Drag marker untuk mengubah lokasi</small>
|
||||||
|
</div>
|
||||||
|
`).openPopup();
|
||||||
|
|
||||||
|
// Handle marker drag
|
||||||
|
this.currentMarker.on('dragend', (e) => {
|
||||||
|
const marker = e.target;
|
||||||
|
const position = marker.getLatLng();
|
||||||
|
|
||||||
|
// Update input values
|
||||||
|
if (this.latInput) this.latInput.value = position.lat.toFixed(8);
|
||||||
|
if (this.lngInput) this.lngInput.value = position.lng.toFixed(8);
|
||||||
|
|
||||||
|
// Update popup content
|
||||||
|
marker.setPopupContent(`
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<strong>Lokasi Dipilih</strong><br>
|
||||||
|
<small>${position.lat.toFixed(8)}, ${position.lng.toFixed(8)}</small><br>
|
||||||
|
<small style="color: #6b7280; margin-top: 4px; display: block;">Drag marker untuk mengubah lokasi</small>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Fetch address for new position
|
||||||
|
this.fetchAddress(position.lat, position.lng);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Center map on marker
|
||||||
|
this.map.setView([lat, lng], 15);
|
||||||
|
|
||||||
|
// Fetch address if requested
|
||||||
|
if (fetchAddr) {
|
||||||
|
this.fetchAddress(lat, lng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API methods
|
||||||
|
getCoordinates() {
|
||||||
|
return {
|
||||||
|
lat: parseFloat(this.latInput?.value),
|
||||||
|
lng: parseFloat(this.lngInput?.value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setCoordinates(lat, lng, updateMarker = true) {
|
||||||
|
if (this.latInput) this.latInput.value = lat.toFixed(8);
|
||||||
|
if (this.lngInput) this.lngInput.value = lng.toFixed(8);
|
||||||
|
|
||||||
|
if (updateMarker) {
|
||||||
|
this.updateMarker(lat, lng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
// Clean up event listeners and map
|
||||||
|
if (this.map) {
|
||||||
|
this.map.remove();
|
||||||
|
}
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in modules or expose globally
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = LeafletMapPicker;
|
||||||
|
} else {
|
||||||
|
window.LeafletMapPicker = LeafletMapPicker;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue