// Location search functionality for the map
class LocationSearch {
constructor(map, apiKey) {
this.map = map;
this.apiKey = apiKey;
this.searchResults = [];
this.searchMarkersLayer = null;
this.currentSearchQuery = '';
this.searchTimeout = null;
this.suggestionsVisible = false;
this.currentSuggestionIndex = -1;
this.initializeSearchBar();
}
initializeSearchBar() {
// Create search toggle button using Leaflet control (positioned below settings button)
const SearchToggleControl = L.Control.extend({
onAdd: function(map) {
const button = L.DomUtil.create('button', 'location-search-toggle');
button.innerHTML = '🔍';
button.style.width = '48px';
button.style.height = '48px';
button.style.border = 'none';
button.style.cursor = 'pointer';
button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
button.style.backgroundColor = 'white';
button.style.borderRadius = '4px';
button.style.padding = '0';
button.style.fontSize = '18px';
button.style.marginTop = '10px'; // Space below settings button
button.title = 'Search locations';
button.id = 'location-search-toggle';
return button;
}
});
// Add the search toggle control to the map
this.map.addControl(new SearchToggleControl({ position: 'topleft' }));
// Use setTimeout to ensure the DOM element is available
setTimeout(() => {
// Get reference to the created button
const toggleButton = document.getElementById('location-search-toggle');
if (toggleButton) {
// Create inline search bar
this.createInlineSearchBar();
// Store references
this.toggleButton = toggleButton;
this.searchVisible = false;
// Bind events
this.bindSearchEvents();
console.log('LocationSearch: Search button initialized successfully');
} else {
console.error('LocationSearch: Could not find search toggle button');
}
}, 100);
}
createInlineSearchBar() {
// Create inline search bar that appears next to the search button
const searchBar = document.createElement('div');
searchBar.className = 'location-search-bar absolute bg-white border border-gray-300 rounded-lg shadow-lg';
searchBar.id = 'location-search-bar';
searchBar.style.width = '400px'; // Increased width for better usability
searchBar.style.padding = '12px'; // Increased padding
searchBar.style.display = 'none'; // Start hidden with inline style instead of class
searchBar.style.zIndex = '9999'; // Very high z-index to ensure visibility
searchBar.innerHTML = `
Suggestions
Results
`;
// Add search bar to the map container
this.map.getContainer().appendChild(searchBar);
// Store references
this.searchBar = searchBar;
this.searchInput = document.getElementById('location-search-input');
this.searchButton = document.getElementById('location-search-submit');
this.closeButton = document.getElementById('location-search-close');
this.suggestionsContainer = document.getElementById('location-search-suggestions');
this.suggestionsPanel = document.getElementById('location-search-suggestions-panel');
this.resultsContainer = document.getElementById('location-search-results');
this.resultsPanel = document.getElementById('location-search-results-panel');
// No clear button or default panel in inline mode
this.clearButton = null;
this.defaultPanel = null;
}
bindSearchEvents() {
// Toggle search bar visibility
this.toggleButton.addEventListener('click', (e) => {
console.log('Search button clicked!');
e.preventDefault();
e.stopPropagation();
this.showSearchBar();
});
// Close search bar
this.closeButton.addEventListener('click', () => {
this.hideSearchBar();
});
// Search on button click
this.searchButton.addEventListener('click', () => {
this.performSearch();
});
// Search on Enter key
this.searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
if (this.suggestionsVisible && this.currentSuggestionIndex >= 0) {
this.selectSuggestion(this.currentSuggestionIndex);
} else {
this.performSearch();
}
}
});
// Clear search (no clear button in inline mode, handled by close button)
// Handle real-time suggestions
this.searchInput.addEventListener('input', (e) => {
const query = e.target.value.trim();
if (query.length > 0) {
this.debouncedSuggestionSearch(query);
} else {
this.hideSuggestions();
this.showDefaultState();
}
});
// Handle keyboard navigation for suggestions
this.searchInput.addEventListener('keydown', (e) => {
if (this.suggestionsVisible) {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.navigateSuggestions(1);
break;
case 'ArrowUp':
e.preventDefault();
this.navigateSuggestions(-1);
break;
case 'Escape':
this.hideSuggestions();
this.showDefaultState();
break;
}
}
});
// Close sidepanel on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.searchVisible) {
this.hideSearchBar();
}
});
// Close search bar when clicking outside (but not on map interactions)
document.addEventListener('click', (e) => {
if (this.searchVisible &&
!e.target.closest('.location-search-bar') &&
!e.target.closest('#location-search-toggle') &&
!e.target.closest('.leaflet-container')) { // Don't close on map interactions
this.hideSearchBar();
}
});
// Maintain search bar position during map movements
this.map.on('movestart zoomstart', () => {
if (this.searchVisible) {
// Store current button position before map movement
this.storedButtonPosition = this.toggleButton.getBoundingClientRect();
}
});
// Reposition search bar after map movements to maintain relative position
this.map.on('moveend zoomend', () => {
if (this.searchVisible && this.storedButtonPosition) {
// Recalculate position based on new button position
this.repositionSearchBar();
}
});
}
async performSearch() {
const query = this.searchInput.value.trim();
if (!query) return;
this.currentSearchQuery = query;
this.showLoading();
try {
const response = await fetch(`/api/v1/locations?q=${encodeURIComponent(query)}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Search failed: ${response.status} ${response.statusText}`);
}
const data = await response.json();
this.displaySearchResults(data);
} catch (error) {
console.error('Location search error:', error);
this.showError('Failed to search locations. Please try again.');
}
}
showLoading() {
// Hide other panels and show results with loading
this.suggestionsPanel.classList.add('hidden');
this.resultsPanel.classList.remove('hidden');
this.resultsContainer.innerHTML = `
Searching for "${this.currentSearchQuery}"...
`;
}
showError(message) {
// Hide other panels and show results with error
this.suggestionsPanel.classList.add('hidden');
this.resultsPanel.classList.remove('hidden');
this.resultsContainer.innerHTML = `
⚠️
Search Failed
${message}
`;
}
displaySearchResults(data) {
// Hide other panels and show results
this.suggestionsPanel.classList.add('hidden');
this.resultsPanel.classList.remove('hidden');
if (!data.locations || data.locations.length === 0) {
this.resultsContainer.innerHTML = `
`;
document.body.appendChild(notification);
// Auto-remove notification after 10 seconds (longer duration)
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 10000);
}
clearSearch() {
this.searchInput.value = '';
this.hideResults();
this.clearSearchMarkers();
this.clearVisitMarker();
this.currentSearchQuery = '';
}
clearVisitMarker() {
if (this.visitMarker) {
this.map.removeLayer(this.visitMarker);
this.visitMarker = null;
}
if (this.currentVisitMarker) {
this.map.removeLayer(this.currentVisitMarker);
this.currentVisitMarker = null;
}
// Remove any visit notifications
const existingNotification = document.querySelector('.visit-navigation-notification');
if (existingNotification) {
existingNotification.remove();
}
}
showSearchBar() {
console.log('showSearchBar called');
if (!this.searchBar) {
console.error('Search bar element not found!');
return;
}
// Position the search bar to the right of the search button at same height
const buttonRect = this.toggleButton.getBoundingClientRect();
const mapRect = this.map.getContainer().getBoundingClientRect();
// Calculate position relative to the map container
const left = buttonRect.right - mapRect.left + 15; // 15px gap to the right of button
const top = buttonRect.top - mapRect.top; // Same height as button
console.log('Positioning search bar at:', { left, top });
// Position search bar next to the button
this.searchBar.style.left = left + 'px';
this.searchBar.style.top = top + 'px';
this.searchBar.style.transform = 'none'; // Remove any transforms
this.searchBar.style.position = 'absolute'; // Position relative to map container
// Show the search bar
this.searchBar.style.setProperty('display', 'block', 'important');
this.searchBar.style.visibility = 'visible';
this.searchBar.style.opacity = '1';
this.searchVisible = true;
console.log('Search bar positioned next to button');
// Focus the search input for immediate typing
setTimeout(() => {
if (this.searchInput) {
this.searchInput.focus();
}
}, 100);
}
repositionSearchBar() {
if (!this.searchBar || !this.searchVisible) return;
// Get current button position after map movement
const buttonRect = this.toggleButton.getBoundingClientRect();
const mapRect = this.map.getContainer().getBoundingClientRect();
// Calculate new position
const left = buttonRect.right - mapRect.left + 15;
const top = buttonRect.top - mapRect.top;
// Update search bar position
this.searchBar.style.left = left + 'px';
this.searchBar.style.top = top + 'px';
console.log('Search bar repositioned after map movement');
}
hideSearchBar() {
this.searchBar.style.display = 'none';
this.searchVisible = false;
this.clearSearch();
this.hideResults();
this.hideSuggestions();
}
showDefaultState() {
// No default panel in inline mode, just hide suggestions and results
this.hideSuggestions();
this.hideResults();
}
clearSearchMarkers() {
if (this.searchMarkersLayer) {
this.map.removeLayer(this.searchMarkersLayer);
this.searchMarkersLayer = null;
}
}
hideResults() {
if (this.resultsPanel) {
this.resultsPanel.classList.add('hidden');
}
}
// Suggestion-related methods
debouncedSuggestionSearch(query) {
// Clear existing timeout
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
// Set new timeout for debounced search
this.searchTimeout = setTimeout(() => {
this.performSuggestionSearch(query);
}, 300); // 300ms debounce delay
}
async performSuggestionSearch(query) {
if (query.length < 2) {
this.hideSuggestions();
return;
}
// Show loading state for suggestions
this.showSuggestionsLoading();
try {
const response = await fetch(`/api/v1/locations/suggestions?q=${encodeURIComponent(query)}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`Suggestions failed: ${response.status}`);
}
const data = await response.json();
this.displaySuggestions(data.suggestions || []);
} catch (error) {
console.error('Suggestion search error:', error);
this.hideSuggestions();
}
}
showSuggestionsLoading() {
// Hide other panels and show suggestions with loading
this.resultsPanel.classList.add('hidden');
this.suggestionsPanel.classList.remove('hidden');
this.suggestionsContainer.innerHTML = `
⏳
Finding suggestions...
`;
}
displaySuggestions(suggestions) {
if (!suggestions.length) {
this.hideSuggestions();
return;
}
// Hide other panels and show suggestions
this.resultsPanel.classList.add('hidden');
this.suggestionsPanel.classList.remove('hidden');
// Build suggestions HTML
let suggestionsHtml = '';
suggestions.forEach((suggestion, index) => {
const isActive = index === this.currentSuggestionIndex;
suggestionsHtml += `