197 lines
5.9 KiB
JavaScript
197 lines
5.9 KiB
JavaScript
/**
|
||
* Map Search Module
|
||
* Handles address search functionality for the map
|
||
*/
|
||
|
||
import { map } from './map-manager.js';
|
||
import { openAddModal } from './location-manager.js';
|
||
|
||
export class MapSearch {
|
||
constructor() {
|
||
this.searchCache = new Map();
|
||
this.tempMarker = null;
|
||
}
|
||
|
||
/**
|
||
* Search for addresses using the geocoding API
|
||
* @param {string} query - The search query
|
||
* @returns {Promise<Array>} Array of search results
|
||
*/
|
||
async search(query) {
|
||
if (!query || query.trim().length < 2) {
|
||
return [];
|
||
}
|
||
|
||
const trimmedQuery = query.trim();
|
||
|
||
// Check cache first
|
||
if (this.searchCache.has(trimmedQuery)) {
|
||
return this.searchCache.get(trimmedQuery);
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`/api/geocode/search?query=${encodeURIComponent(trimmedQuery)}&limit=5`);
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Search failed: ${response.statusText}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (!data.success) {
|
||
throw new Error(data.error || 'Search failed');
|
||
}
|
||
|
||
const results = data.data || [];
|
||
|
||
// Cache the results
|
||
this.searchCache.set(trimmedQuery, results);
|
||
|
||
// Clean up cache if it gets too large
|
||
if (this.searchCache.size > 100) {
|
||
const firstKey = this.searchCache.keys().next().value;
|
||
this.searchCache.delete(firstKey);
|
||
}
|
||
|
||
return results;
|
||
|
||
} catch (error) {
|
||
console.error('Map search error:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create HTML for a search result
|
||
* @param {Object} result - Search result object
|
||
* @returns {HTMLElement} Result element
|
||
*/
|
||
createResultElement(result) {
|
||
const resultEl = document.createElement('div');
|
||
resultEl.className = 'search-result-item search-result-map';
|
||
|
||
// Handle both coordinate formats for compatibility
|
||
const lat = result.coordinates?.lat || result.latitude || 0;
|
||
const lng = result.coordinates?.lng || result.longitude || 0;
|
||
|
||
// Debugging - log result structure if coordinates are missing
|
||
if (!lat && !lng) {
|
||
console.warn('Search result missing coordinates:', result);
|
||
}
|
||
|
||
resultEl.innerHTML = `
|
||
<div class="result-address">${result.formattedAddress || 'Unknown Address'}</div>
|
||
<div class="result-full-address">${result.fullAddress || ''}</div>
|
||
<div class="result-coordinates">${lat.toFixed(6)}, ${lng.toFixed(6)}</div>
|
||
`;
|
||
|
||
resultEl.addEventListener('click', () => {
|
||
this.selectResult(result);
|
||
});
|
||
|
||
return resultEl;
|
||
}
|
||
|
||
/**
|
||
* Handle selection of a search result
|
||
* @param {Object} result - Selected result
|
||
*/
|
||
selectResult(result) {
|
||
if (!map) {
|
||
console.error('Map not available');
|
||
return;
|
||
}
|
||
|
||
// Handle both coordinate formats for compatibility
|
||
const lat = parseFloat(result.coordinates?.lat || result.latitude || 0);
|
||
const lng = parseFloat(result.coordinates?.lng || result.longitude || 0);
|
||
|
||
if (isNaN(lat) || isNaN(lng)) {
|
||
console.error('Invalid coordinates in result:', result);
|
||
return;
|
||
}
|
||
|
||
// Pan and zoom to the location
|
||
map.setView([lat, lng], 16);
|
||
|
||
// Remove any existing temporary marker
|
||
this.clearTempMarker();
|
||
|
||
// Add a temporary marker
|
||
this.tempMarker = L.marker([lat, lng], {
|
||
icon: L.divIcon({
|
||
className: 'temp-search-marker',
|
||
html: '📍',
|
||
iconSize: [30, 30],
|
||
iconAnchor: [15, 30]
|
||
})
|
||
}).addTo(map);
|
||
|
||
// Create popup with add location option
|
||
const popupContent = `
|
||
<div class="search-result-popup">
|
||
<h3>${result.formattedAddress || 'Search Result'}</h3>
|
||
<p>${result.fullAddress || ''}</p>
|
||
<div class="popup-actions">
|
||
<button class="btn btn-success btn-sm" onclick="mapSearchInstance.openAddLocationModal(${lat}, ${lng})">
|
||
➕ Add Location Here
|
||
</button>
|
||
<button class="btn btn-secondary btn-sm" onclick="mapSearchInstance.clearTempMarker()">
|
||
✕ Clear
|
||
</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
this.tempMarker.bindPopup(popupContent).openPopup();
|
||
|
||
// Auto-clear the marker after 30 seconds
|
||
setTimeout(() => {
|
||
this.clearTempMarker();
|
||
}, 30000);
|
||
}
|
||
|
||
/**
|
||
* Open the add location modal at specified coordinates
|
||
* @param {number} lat - Latitude
|
||
* @param {number} lng - Longitude
|
||
*/
|
||
openAddLocationModal(lat, lng) {
|
||
this.clearTempMarker();
|
||
|
||
if (typeof openAddModal === 'function') {
|
||
openAddModal(lat, lng);
|
||
} else {
|
||
// Fallback: trigger the add location button click
|
||
const addBtn = document.getElementById('add-location-btn');
|
||
if (addBtn) {
|
||
// Set a temporary flag for the coordinates
|
||
window.tempSearchCoordinates = { lat, lng };
|
||
addBtn.click();
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clear the temporary search marker
|
||
*/
|
||
clearTempMarker() {
|
||
if (this.tempMarker && map) {
|
||
map.removeLayer(this.tempMarker);
|
||
this.tempMarker = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clear the search cache
|
||
*/
|
||
clearCache() {
|
||
this.searchCache.clear();
|
||
}
|
||
}
|
||
|
||
// Create a global instance for use in popup buttons
|
||
window.mapSearchInstance = new MapSearch();
|
||
|
||
export default window.mapSearchInstance;
|