// Map view — главный экран
const MapView = {
map: null,
markers: [],
placeMarkers: [], // маркеры баров/ресторанов
placeMarkersLayer: null,
radius: 5,
visible: true,
nearby: [],
places: [],
showPlaces: true,
async render(root) {
root.innerHTML = `
🗺️ Карта
Нажми чтобы получить координаты
—
`;
// если старый map привязан к удалённому div — убьём его и создадим новый
if (this.map) {
try {
const container = this.map.getContainer();
if (!container || !document.body.contains(container)) {
this.map.remove();
this.map = null;
this.placeMarkersLayer = null;
this.markers = [];
}
} catch (e) {
this.map = null;
}
}
if (!this.map) {
this.map = L.map('map').setView([55.7558, 37.6173], 12);
// 2GIS tile layer
L.tileLayer('https://tile2.maps.2gis.com/tiles?x={x}&y={y}&z={z}', {
attribution: '© 2GIS',
maxZoom: 19,
}).addTo(this.map);
}
setTimeout(() => this.map.invalidateSize(), 100);
document.getElementById('visible-toggle').addEventListener('change', async (e) => {
this.visible = e.target.checked;
try {
await API.Auth.setVisibility(this.visible);
if (window._lastKnownPos) this.applyPosition(window._lastKnownPos);
} catch (err) {
console.warn(err);
TG_APP.showAlert('Не удалось обновить видимость');
}
});
document.getElementById('radius-sel').addEventListener('change', async (e) => {
this.radius = parseInt(e.target.value, 10);
document.getElementById('radius-label') && (document.getElementById('radius-label').textContent = this.radius + ' км');
if (window._lastKnownPos) {
await this.refreshNearbyFromPos(window._lastKnownPos.coords.latitude, window._lastKnownPos.coords.longitude);
await this.refreshPlacesFromPos(window._lastKnownPos.coords.latitude, window._lastKnownPos.coords.longitude);
}
});
document.getElementById('loc-share-btn').onclick = (e) => {
e.preventDefault();
this.requestLocation();
};
document.getElementById('places-toggle').onclick = () => {
this.showPlaces = !this.showPlaces;
const txt = document.getElementById('places-toggle-text');
if (txt) txt.textContent = this.showPlaces ? 'Скрыть бары' : 'Показать бары';
if (this.placeMarkersLayer) {
if (this.showPlaces) this.map.addLayer(this.placeMarkersLayer);
else this.map.removeLayer(this.placeMarkersLayer);
}
const list = document.getElementById('places-list');
if (list) list.style.display = this.showPlaces ? 'block' : 'none';
};
// если уже есть сохранённые координаты — покажем на карте (быстро)
if (window._lastKnownPos) {
this.applyPosition(window._lastKnownPos);
}
// НЕ запрашиваем автоматически — это блокирует UI на iOS Safari.
// Юзер нажмёт кнопку "📍 Поделиться геолокацией".
},
requestLocation() {
if (!navigator.geolocation) {
const s = document.getElementById('loc-status');
if (s) s.textContent = 'Геолокация недоступна в браузере';
return;
}
const btn = document.getElementById('loc-share-btn');
const status = document.getElementById('loc-status');
if (btn) { btn.disabled = true; btn.textContent = '⏳ Запрашиваю…'; }
if (status) status.textContent = 'Браузер спросит разрешение…';
navigator.geolocation.getCurrentPosition(
async (pos) => {
window._lastKnownPos = pos;
if (btn) {
btn.disabled = false;
btn.innerHTML = '📍 Обновить геолокацию';
}
if (status) status.textContent = `✓ ${pos.coords.latitude.toFixed(4)}, ${pos.coords.longitude.toFixed(4)}`;
await this.applyPosition(pos);
},
(err) => {
if (btn) {
btn.disabled = false;
btn.innerHTML = '📍 Попробовать снова';
}
if (status) {
const msg = this.geolocationErrorText(err);
status.innerHTML = '⚠️ ' + msg + (err.code === 1
? '
Открой Настройки → Сайты → Геолокация → buhapp.mygoodservice.ru'
: '');
}
console.warn('geolocation error:', err);
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 }
);
},
geolocationErrorText(err) {
switch (err.code) {
case err.PERMISSION_DENIED: return 'Доступ к геолокации запрещён.';
case err.POSITION_UNAVAILABLE: return 'Не удалось определить координаты (нет GPS/Wi-Fi).';
case err.TIMEOUT: return 'Таймаут запроса геолокации.';
default: return 'Неизвестная ошибка геолокации.';
}
},
async applyPosition(pos) {
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
try {
await API.Auth.updateLocation(lat, lng, this.visible);
} catch (e) {
console.warn('updateLocation:', e);
}
if (this.map) {
// важно: invalidateSize() чтобы Leaflet пересчитал размеры (после смены таба лейаут мог обнулиться)
this.map.invalidateSize();
// убираем старый self-marker и круг
if (this.selfMarker) { this.map.removeLayer(this.selfMarker); this.selfMarker = null; }
this.map.eachLayer((layer) => {
if (layer instanceof L.Circle && layer.options && layer.options._isRadius) {
this.map.removeLayer(layer);
}
});
// маркер "Я"
const meIcon = L.divIcon({
className: 'self-marker',
html: '',
iconSize: [18, 18],
iconAnchor: [9, 9],
});
this.selfMarker = L.marker([lat, lng], { icon: meIcon, zIndexOffset: 1000 }).addTo(this.map);
// круг радиуса
L.circle([lat, lng], {
radius: this.radius * 1000,
color: '#FF6B35',
fillColor: '#FF6B35',
fillOpacity: 0.1,
weight: 2,
_isRadius: true,
}).addTo(this.map);
// центр + zoom на мне
this.map.setView([lat, lng], 14, { animate: true });
this.refreshNearbyFromPos(lat, lng);
this.refreshPlacesFromPos(lat, lng);
}
},
async refreshNearby() {
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(async (pos) => {
this.refreshNearbyFromPos(pos.coords.latitude, pos.coords.longitude);
});
},
async refreshNearbyFromPos(lat, lng) {
try {
const r = await API.Auth.searchNearby(lat, lng, this.radius);
this.nearby = r.results || [];
const c = document.getElementById('nearby-count');
if (c) c.textContent = '👥 ' + this.nearby.length + ' чел. рядом';
this.markers.forEach(m => this.map.removeLayer(m));
this.markers = [];
this.nearby.forEach((n) => {
const icon = L.divIcon({
className: 'custom-marker',
html: `${escapeHtml((n.name || '?')[0])}
`,
iconSize: [36, 36],
});
const m = L.marker([n.lat, n.lng], { icon }).addTo(this.map);
m.bindTooltip(n.name + ' • ' + (n.distance_m / 1000).toFixed(1) + ' км', { direction: 'top' });
m.on('click', () => App.openUserProfile(n.user_id, n.name));
this.markers.push(m);
});
} catch (e) {
const c = document.getElementById('nearby-count');
if (c) c.textContent = '📍 нужна геолокация';
console.warn(e);
}
},
async refreshPlacesFromPos(lat, lng) {
try {
const r = await API.PlacesApi.nearby(lat, lng, this.radius, 50);
this.places = r.places || [];
// маркеры
if (this.placeMarkersLayer) this.map.removeLayer(this.placeMarkersLayer);
this.placeMarkersLayer = L.layerGroup();
this.places.forEach((p) => {
const icon = L.divIcon({
className: 'place-marker',
html: `${placeEmoji(p.type)}
`,
iconSize: [32, 32],
});
const m = L.marker([p.lat, p.lng], { icon });
m.bindTooltip(
`${escapeHtml(p.name)}
${placeTypeName(p.type)} • ${(p.distance_m/1000).toFixed(1)} км${p.cuisine ? '
🍴 ' + escapeHtml(p.cuisine) : ''}`,
{ direction: 'top' }
);
m.on('click', () => this.openPlaceCard(p));
this.placeMarkersLayer.addLayer(m);
});
if (this.showPlaces) this.placeMarkersLayer.addTo(this.map);
// список снизу
this.renderPlacesList();
} catch (e) {
console.warn('refreshPlaces:', e);
}
},
renderPlacesList() {
const box = document.getElementById('places-list');
if (!box) return;
if (!this.places.length) {
box.innerHTML = 'Бары рядом не найдены. Попробуй увеличить радиус.
';
return;
}
box.innerHTML = '🍻 Где выпить / поесть:
' +
this.places.map((p) => `
${placeEmoji(p.type)}
${escapeHtml(p.name)}
${placeTypeName(p.type)} • ${(p.distance_m/1000).toFixed(1)} км${p.cuisine ? ' • ' + escapeHtml(p.cuisine) : ''}
`).join('');
// клики по карточкам
box.querySelectorAll('.card.clickable').forEach((el) => {
el.onclick = () => {
const id = parseInt(el.dataset.placeId, 10);
const p = this.places.find(x => x.osm_id === id);
if (p) this.openPlaceCard(p);
};
});
box.querySelectorAll('[data-invite-place]').forEach((btn) => {
btn.onclick = (e) => {
e.stopPropagation();
const id = parseInt(btn.dataset.invitePlace, 10);
const p = this.places.find(x => x.osm_id === id);
if (p) this.inviteToPlace(p);
};
});
},
openPlaceCard(p) {
const msg = `${p.name} (${placeTypeName(p.type)})\n📍 ${p.lat.toFixed(4)}, ${p.lng.toFixed(4)}${p.cuisine ? '\n🍴 ' + p.cuisine : ''}${p.hours ? '\n🕐 ' + p.hours : ''}${p.phone ? '\n📞 ' + p.phone : ''}`;
TG_APP.showAlert(msg);
},
async inviteToPlace(p) {
// открываем список чатов для выбора кому предложить встречу
const chats = await API.ChatApi.listChats();
if (!chats.chats || !chats.chats.length) {
TG_APP.showAlert('Сначала начни чат с кем-нибудь');
return;
}
// простая реализация — показать список через confirm
const list = chats.chats.slice(0, 5).map((c, i) => `${i+1}. ${c.other_name || c.otherName || 'собеседник'}`).join('\n');
TG_APP.showConfirm(
`Отправить предложение встретиться в "${p.name}"?\n\nТвои чаты:\n${list}\n\nПредложение будет отправлено в самый новый чат.`
).then(async (ok) => {
if (!ok) return;
const target = chats.chats[0];
const text = `🍻 Давай встретимся здесь: ${p.name}\n📍 https://2gis.ru/?m=${p.lng.toFixed(6)}%2C${p.lat.toFixed(6)}%2F16\n${p.address ? '🏠 ' + p.address : ''}`;
try {
await API.ChatApi.sendMessage(target.chat.id || target.chat.id, text);
TG_APP.showAlert('Приглашение отправлено в чат с ' + (target.other_name || 'собеседником'));
} catch (e) {
TG_APP.showAlert('Ошибка: ' + e.message);
}
});
},
};
function placeEmoji(type) {
switch (type) {
case 'bar':
case 'pub':
case 'biergarten': return '🍺';
case 'restaurant': return '🍽️';
case 'cafe': return '☕';
case 'nightclub': return '🎉';
default: return '🍴';
}
}
function placeTypeName(type) {
switch (type) {
case 'bar': return 'Бар';
case 'pub': return 'Паб';
case 'biergarten': return 'Пивная';
case 'restaurant': return 'Ресторан';
case 'cafe': return 'Кафе';
case 'nightclub': return 'Ночной клуб';
default: return 'Заведение';
}
}
window.MapView = MapView;