488 lines
No EOL
18 KiB
JavaScript
488 lines
No EOL
18 KiB
JavaScript
class MapSearch {
|
|
constructor(map) {
|
|
this.map = map;
|
|
this.searchResults = [];
|
|
this.currentMarkers = [];
|
|
this.userLocation = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
this.createSearchUI();
|
|
this.bindEvents();
|
|
this.getUserLocation();
|
|
}
|
|
|
|
createSearchUI() {
|
|
// ایجاد پنل جستجو
|
|
const searchPanel = document.createElement('div');
|
|
searchPanel.className = 'search-panel';
|
|
searchPanel.innerHTML = `
|
|
<div class="search-header">
|
|
<h3>جستجو در نقشه</h3>
|
|
<button class="close-btn" onclick="this.parentElement.parentElement.style.display='none'">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="search-input-container">
|
|
<div class="search-input-wrapper">
|
|
<input type="text" id="searchInput" placeholder="جستجو در مکانها، خیابانها..." />
|
|
<button id="searchBtn" class="search-icon-btn" title="جستجو">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="search-filters">
|
|
<select id="searchType" class="search-select">
|
|
<option value="all">همه</option>
|
|
<option value="points">مکانها</option>
|
|
<option value="streets">خیابانها</option>
|
|
<option value="areas">مناطق</option>
|
|
</select>
|
|
<button id="locationBtn" class="location-btn" title="استفاده از موقعیت من">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div id="searchResults" class="search-results"></div>
|
|
`;
|
|
|
|
// اضافه کردن استایل
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.search-panel {
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
width: 380px;
|
|
background: white;
|
|
border-radius: 12px;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
|
|
z-index: 1000;
|
|
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
direction: rtl;
|
|
display: none;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
overflow: hidden;
|
|
transform: translateZ(0);
|
|
}
|
|
.search-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 16px 20px;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
}
|
|
.search-header h3 {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
}
|
|
.close-btn {
|
|
background: rgba(255,255,255,0.2);
|
|
border: none;
|
|
border-radius: 50%;
|
|
width: 28px;
|
|
height: 28px;
|
|
cursor: pointer;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.close-btn:hover {
|
|
background: rgba(255,255,255,0.3);
|
|
transform: scale(1.05);
|
|
}
|
|
.search-input-container {
|
|
padding: 16px 20px;
|
|
}
|
|
.search-input-wrapper {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
#searchInput {
|
|
flex: 1;
|
|
padding: 12px 16px;
|
|
padding-left: 48px;
|
|
border: 2px solid #e1e5e9;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
transition: all 0.2s ease;
|
|
background: #f8f9fa;
|
|
}
|
|
#searchInput:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
background: white;
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
}
|
|
.search-icon-btn {
|
|
position: absolute;
|
|
left: 12px;
|
|
background: none;
|
|
border: none;
|
|
color: #667eea;
|
|
cursor: pointer;
|
|
padding: 4px;
|
|
border-radius: 4px;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.search-icon-btn:hover {
|
|
background: rgba(102, 126, 234, 0.1);
|
|
transform: scale(1.05);
|
|
}
|
|
.search-filters {
|
|
padding: 0 20px 16px;
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
}
|
|
.search-select {
|
|
flex: 1;
|
|
padding: 10px 12px;
|
|
border: 2px solid #e1e5e9;
|
|
border-radius: 8px;
|
|
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
font-size: 13px;
|
|
background: #f8f9fa;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.search-select:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
background: white;
|
|
}
|
|
.location-btn {
|
|
padding: 10px;
|
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 16px;
|
|
transition: all 0.2s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.location-btn:hover {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3);
|
|
}
|
|
.search-results {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
border-top: 1px solid #f0f0f0;
|
|
}
|
|
.search-result-item {
|
|
padding: 12px 20px;
|
|
border-bottom: 1px solid #f8f9fa;
|
|
cursor: pointer;
|
|
transition: padding-right 0.2s ease, background-color 0.2s ease;
|
|
}
|
|
.search-result-item:hover {
|
|
background: #f8f9fa;
|
|
padding-right: 22px;
|
|
}
|
|
.search-result-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.result-name {
|
|
font-weight: 600;
|
|
color: #333;
|
|
margin-bottom: 4px;
|
|
font-size: 14px;
|
|
}
|
|
.result-category {
|
|
color: #666;
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
}
|
|
.result-city {
|
|
color: #28a745;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
margin-top: 2px;
|
|
}
|
|
.result-distance {
|
|
color: #007bff;
|
|
font-size: 12px;
|
|
margin-top: 4px;
|
|
font-weight: 500;
|
|
}
|
|
.no-results {
|
|
padding: 24px 20px;
|
|
text-align: center;
|
|
color: #666;
|
|
font-weight: 400;
|
|
}
|
|
.loading {
|
|
padding: 24px 20px;
|
|
text-align: center;
|
|
color: #666;
|
|
font-weight: 400;
|
|
}
|
|
.loading::after {
|
|
content: '';
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid #667eea;
|
|
border-radius: 50%;
|
|
border-top-color: transparent;
|
|
animation: spin 1s linear infinite;
|
|
margin-right: 8px;
|
|
}
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
`;
|
|
|
|
document.head.appendChild(style);
|
|
document.body.appendChild(searchPanel);
|
|
}
|
|
|
|
bindEvents() {
|
|
const searchInput = document.getElementById('searchInput');
|
|
const searchBtn = document.getElementById('searchBtn');
|
|
const locationBtn = document.getElementById('locationBtn');
|
|
|
|
// جستجو با Enter
|
|
searchInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
this.performSearch();
|
|
}
|
|
});
|
|
|
|
// جستجو با کلیک
|
|
searchBtn.addEventListener('click', () => {
|
|
this.performSearch();
|
|
});
|
|
|
|
// استفاده از موقعیت کاربر
|
|
locationBtn.addEventListener('click', () => {
|
|
this.getUserLocation();
|
|
});
|
|
|
|
// Autocomplete
|
|
let timeout;
|
|
searchInput.addEventListener('input', () => {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => {
|
|
if (searchInput.value.length >= 2) {
|
|
this.performSearch();
|
|
}
|
|
}, 500);
|
|
});
|
|
}
|
|
|
|
async performSearch() {
|
|
const query = document.getElementById('searchInput').value.trim();
|
|
const type = document.getElementById('searchType').value;
|
|
|
|
if (!query) {
|
|
this.showResults([]);
|
|
return;
|
|
}
|
|
|
|
this.showLoading();
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
q: query,
|
|
type: type,
|
|
limit: 20
|
|
});
|
|
|
|
// اضافه کردن موقعیت کاربر اگر موجود باشد
|
|
if (this.userLocation) {
|
|
params.append('lat', this.userLocation.lat);
|
|
params.append('lng', this.userLocation.lng);
|
|
}
|
|
|
|
const response = await fetch(`/api/search/places?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this.searchResults = data.results;
|
|
this.showResults(data.results);
|
|
this.addMarkersToMap(data.results);
|
|
} else {
|
|
this.showError(data.error || 'خطا در جستجو');
|
|
}
|
|
} catch (error) {
|
|
console.error('Search error:', error);
|
|
this.showError('خطا در اتصال به سرور');
|
|
}
|
|
}
|
|
|
|
showResults(results) {
|
|
const resultsContainer = document.getElementById('searchResults');
|
|
|
|
if (results.length === 0) {
|
|
resultsContainer.innerHTML = '<div class="no-results">نتیجهای یافت نشد</div>';
|
|
return;
|
|
}
|
|
|
|
const resultsHtml = results.map(result => `
|
|
<div class="search-result-item" onclick="mapSearch.navigateToResult(${result.lat}, ${result.lng}, '${result.name}')">
|
|
<div class="result-name">${this.escapeHtml(result.name)}</div>
|
|
<div class="result-category">${this.getCategoryText(result.category)}</div>
|
|
${result.city && result.city !== 'نامشخص' ? `<div class="result-city">${this.escapeHtml(result.city)}</div>` : ''}
|
|
${result.distance ? `<div class="result-distance">${result.distance} متر</div>` : ''}
|
|
</div>
|
|
`).join('');
|
|
|
|
resultsContainer.innerHTML = resultsHtml;
|
|
}
|
|
|
|
showLoading() {
|
|
const resultsContainer = document.getElementById('searchResults');
|
|
resultsContainer.innerHTML = '<div class="loading">در حال جستجو...</div>';
|
|
}
|
|
|
|
showError(message) {
|
|
const resultsContainer = document.getElementById('searchResults');
|
|
resultsContainer.innerHTML = `<div class="no-results" style="color: #dc3545;">${message}</div>`;
|
|
}
|
|
|
|
addMarkersToMap(results) {
|
|
// حذف مارکرهای قبلی
|
|
this.clearMarkers();
|
|
|
|
results.forEach(result => {
|
|
const marker = L.marker([result.lat, result.lng], {
|
|
icon: this.getMarkerIcon(result.category)
|
|
});
|
|
|
|
const popupContent = `
|
|
<div style="text-align: right; direction: rtl; font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; padding-right: 20px;">
|
|
<h4 style="margin: 0 0 6px 0; font-weight: 600; color: #333; font-size: 14px; line-height: 1.3;">${this.escapeHtml(result.name)}</h4>
|
|
<p style="margin: 3px 0; color: #666; font-size: 12px;">${this.getCategoryText(result.category)}</p>
|
|
${result.city && result.city !== 'نامشخص' ? `<p style="margin: 3px 0; color: #28a745; font-size: 11px; font-weight: 500;">شهر: ${this.escapeHtml(result.city)}</p>` : ''}
|
|
${result.distance ? `<p style="margin: 3px 0; color: #007bff; font-size: 11px; font-weight: 500;">فاصله: ${result.distance} متر</p>` : ''}
|
|
</div>
|
|
`;
|
|
|
|
marker.bindPopup(popupContent);
|
|
marker.addTo(this.map);
|
|
this.currentMarkers.push(marker);
|
|
});
|
|
|
|
// تنظیم زوم و مرکز نقشه اگر نتیجهای وجود دارد
|
|
if (results.length > 0) {
|
|
const group = new L.featureGroup(this.currentMarkers);
|
|
this.map.fitBounds(group.getBounds().pad(0.1));
|
|
}
|
|
}
|
|
|
|
clearMarkers() {
|
|
this.currentMarkers.forEach(marker => {
|
|
this.map.removeLayer(marker);
|
|
});
|
|
this.currentMarkers = [];
|
|
}
|
|
|
|
navigateToResult(lat, lng, name) {
|
|
this.map.setView([lat, lng], 16);
|
|
|
|
// نمایش پاپآپ برای مارکر
|
|
this.currentMarkers.forEach(marker => {
|
|
const markerLatLng = marker.getLatLng();
|
|
if (markerLatLng.lat === lat && markerLatLng.lng === lng) {
|
|
marker.openPopup();
|
|
}
|
|
});
|
|
}
|
|
|
|
getUserLocation() {
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
this.userLocation = {
|
|
lat: position.coords.latitude,
|
|
lng: position.coords.longitude
|
|
};
|
|
|
|
// اضافه کردن مارکر موقعیت کاربر
|
|
this.addUserLocationMarker();
|
|
|
|
console.log('User location:', this.userLocation);
|
|
},
|
|
(error) => {
|
|
console.log('Geolocation error:', error);
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
addUserLocationMarker() {
|
|
if (this.userLocationMarker) {
|
|
this.map.removeLayer(this.userLocationMarker);
|
|
}
|
|
|
|
this.userLocationMarker = L.marker([this.userLocation.lat, this.userLocation.lng], {
|
|
icon: L.divIcon({
|
|
className: 'user-location-marker',
|
|
html: '📍',
|
|
iconSize: [30, 30],
|
|
iconAnchor: [15, 15]
|
|
})
|
|
});
|
|
|
|
this.userLocationMarker.bindPopup('موقعیت شما').addTo(this.map);
|
|
}
|
|
|
|
getMarkerIcon(category) {
|
|
const iconColors = {
|
|
'رستوران': '#ff6b6b',
|
|
'کافه': '#4ecdc4',
|
|
'بانک': '#45b7d1',
|
|
'داروخانه': '#96ceb4',
|
|
'بیمارستان': '#feca57',
|
|
'مدرسه': '#ff9ff3',
|
|
'دانشگاه': '#54a0ff',
|
|
'فروشگاه': '#5f27cd',
|
|
'خیابان': '#00d2d3',
|
|
'پارک': '#10ac84',
|
|
'مکان مذهبی': '#ff6348',
|
|
'تسهیلات': '#747d8c',
|
|
'مکان': '#747d8c'
|
|
};
|
|
|
|
const color = iconColors[category] || '#747d8c';
|
|
|
|
return L.divIcon({
|
|
className: 'custom-marker',
|
|
html: `<div style="background-color: ${color}; width: 20px; height: 20px; border-radius: 50%; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
|
|
iconSize: [20, 20],
|
|
iconAnchor: [10, 10]
|
|
});
|
|
}
|
|
|
|
getCategoryText(category) {
|
|
return category || 'مکان';
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// متغیر سراسری برای دسترسی از HTML
|
|
let mapSearch;
|