feat: leaflet

main
Rohmad Eko Wahyudi 2025-11-12 11:03:49 +07:00
parent 80c4c8e758
commit 29f1c5fb66
No known key found for this signature in database
GPG Key ID: 4CCEDA68CB778BAF
6 changed files with 1017 additions and 7 deletions

View File

@ -1,7 +1,12 @@
@{
@{
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="prose">
<span class="text-xl font-semibold text-black">
@ -274,7 +279,7 @@
<legend class="fieldset-legend">
Latitude
</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>
</div>
<div class="flex flex-col">
@ -282,10 +287,24 @@
<legend class="fieldset-legend">
Longitude
</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>
</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="grid grid-cols-1 space-y-2 md:grid-cols-4 md:space-x-6">
<div class="flex flex-col">
@ -320,4 +339,24 @@
</div>
</form>
</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>
}

View File

@ -2,6 +2,17 @@
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="prose">
<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="flex flex-col">
<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 class="flex flex-col">
<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 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>
@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>
}

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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

View File

@ -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;
}