api/templates/main/map.html.twig
2026-03-17 12:30:28 +00:00

1443 lines
No EOL
67 KiB
Twig
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html.twig' %}
{% block title %}نقشه آنلاین ایران - سرویس می‌مپس | نقشه رستری تعاملی{% endblock %}
{% block description %}نقشه آنلاین ایران با سرویس می‌مپس - نقشه رستری تعاملی با قابلیت زوم، حرکت و جستجو. نقشه ایران با کیفیت بالا و سرعت فوق‌العاده.{% endblock %}
{% block keywords %}نقشه آنلاین ایران, نقشه تعاملی, نقشه رستری, نقشه ایران, نقشه وب, نقشه آنلاین, نقشه تعاملی ایران{% endblock %}
{% block og_title %}نقشه آنلاین ایران - سرویس می‌مپس{% endblock %}
{% block og_description %}نقشه آنلاین ایران با سرویس می‌مپس - نقشه رستری تعاملی با کیفیت بالا{% endblock %}
{% block body %}
<!-- H1 برای سئو (مخفی بصری) -->
<h1 class="visually-hidden">نقشه آنلاین ایران - سرویس می‌مپس | نقشه رستری تعاملی</h1>
<!-- Full Screen Map Container -->
<div id="fullscreen-map-container" class="position-relative">
<!-- Back to Home Button -->
<div class="back-home-btn">
<a href="{{ path('app_main_index') }}" class="map-control-btn" title="بازگشت به صفحه نخست">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/>
</svg>
</a>
</div>
<!-- Map Controls Overlay -->
<div class="map-controls-overlay">
<!-- Search Button -->
<div class="map-control-group">
<button id="search-toggle" class="map-control-btn" title="جستجو در نقشه">
<svg width="16" height="16" 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>
<!-- Zoom Controls -->
<div class="map-control-group">
<button id="zoom-in" class="map-control-btn" title="بزرگنمایی نقشه">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
</svg>
</button>
<button id="zoom-out" class="map-control-btn" title="کوچک‌نمایی نقشه">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 13H5v-2h14v2z"/>
</svg>
</button>
</div>
<!-- Layer Controls -->
<div class="map-control-group">
<button id="layer-toggle" class="map-control-btn" title="تغییر کیفیت نقشه رستری">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 3v18h18V3H3zm16 16H5V5h14v14z"/>
<path d="M7 7h4v4H7zm6 0h4v4h-4zm-6 6h4v4H7zm6 0h4v4h-4z"/>
</svg>
</button>
<button id="google-earth-toggle" class="map-control-btn" title="تغییر به لایه Google Earth">
<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>
<button id="layer-switch" class="map-control-btn" title="تغییر لایه و منبع نقشه (API)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z"/>
</svg>
</button>
</div>
<!-- Layer Switcher Menu (all APIs/layers in one place) -->
<div id="layer-switcher-menu" class="layer-switcher-menu" style="display: none;">
<div class="menu-header">انتخاب لایه و منبع نقشه</div>
<div class="menu-section">
<div class="menu-section-title">OpenStreetMap</div>
<button class="menu-item" data-source="osm" data-layer="standard">
<span class="menu-icon">🌍</span>
<span>OSM استاندارد</span>
</button>
</div>
<div class="menu-section">
<div class="menu-section-title">نقشه رستری (می‌مپس)</div>
<button class="menu-item" data-source="raster" data-layer="hot">
<span class="menu-icon">🗺️</span>
<span>روشن استاندارد</span>
</button>
<button class="menu-item" data-source="raster" data-layer="ret">
<span class="menu-icon">🖼️</span>
<span>روشن رتینا</span>
</button>
<button class="menu-item" data-source="raster" data-layer="dark">
<span class="menu-icon">🌙</span>
<span>تاریک استاندارد</span>
</button>
<button class="menu-item" data-source="raster" data-layer="dark_retina">
<span class="menu-icon">🌃</span>
<span>تاریک رتینا</span>
</button>
</div>
<div class="menu-section">
<div class="menu-section-title">Google Earth (می‌مپس)</div>
<button class="menu-item" data-source="google-earth" data-layer="satellite">
<span class="menu-icon">🛰️</span>
<span>ماهواره‌ای</span>
</button>
<button class="menu-item" data-source="google-earth" data-layer="map">
<span class="menu-icon">🗺️</span>
<span>نقشه</span>
</button>
<button class="menu-item" data-source="google-earth" data-layer="hybrid">
<span class="menu-icon">🌐</span>
<span>ترکیبی</span>
</button>
<button class="menu-item" data-source="google-earth" data-layer="terrain">
<span class="menu-icon">⛰️</span>
<span>زمین‌شناسی</span>
</button>
<button class="menu-item" data-source="google-earth" data-layer="roadmap">
<span class="menu-icon">🛣️</span>
<span>نقشه جاده</span>
</button>
</div>
<div class="menu-section">
<div class="menu-section-title">Bing Maps (می‌مپس)</div>
<button class="menu-item" data-source="bing" data-layer="road">
<span class="menu-icon">🛣️</span>
<span>Bing جاده</span>
</button>
<button class="menu-item" data-source="bing" data-layer="aerial">
<span class="menu-icon">🛰️</span>
<span>Bing ماهواره</span>
</button>
<button class="menu-item" data-source="bing" data-layer="hybrid">
<span class="menu-icon">🌐</span>
<span>Bing ترکیبی</span>
</button>
</div>
<div class="menu-section">
<div class="menu-section-title">لایه‌های وکتوری (می‌مپس)</div>
<button class="menu-item menu-item-overlay" data-overlay="roads" type="button">
<span class="menu-icon">🛣️</span>
<span>جاده‌ها</span>
</button>
<button class="menu-item menu-item-overlay" data-overlay="buildings" type="button">
<span class="menu-icon">🏢</span>
<span>ساختمان‌ها</span>
</button>
<button class="menu-item menu-item-overlay" data-overlay="points" type="button">
<span class="menu-icon">📍</span>
<span>نقاط</span>
</button>
</div>
</div>
<!-- Google Earth Layer Menu (kept for quick toggle from old button) -->
<div id="google-earth-menu" class="google-earth-menu" style="display: none;">
<div class="menu-header">انتخاب لایه Google Earth</div>
<button class="menu-item active" data-layer="satellite">
<span class="menu-icon">🛰️</span>
<span>ماهواره‌ای</span>
</button>
<button class="menu-item" data-layer="map">
<span class="menu-icon">🗺️</span>
<span>نقشه</span>
</button>
<button class="menu-item" data-layer="hybrid">
<span class="menu-icon">🌐</span>
<span>ترکیبی</span>
</button>
<button class="menu-item" data-layer="terrain">
<span class="menu-icon">⛰️</span>
<span>زمین‌شناسی</span>
</button>
<button class="menu-item" data-layer="roadmap">
<span class="menu-icon">🛣️</span>
<span>نقشه جاده</span>
</button>
<button class="menu-item menu-item-back" data-action="back">
<span class="menu-icon">←</span>
<span>بازگشت به نقشه رستری</span>
</button>
</div>
<!-- Theme Controls (Light/Dark) -->
<div class="map-control-group">
<button id="theme-toggle" class="map-control-btn" title="تغییر حالت روشن/تاریک">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<!-- Moon icon -->
<path d="M12.76 3c.41 0 .65.46.43.8A7 7 0 1 0 20.2 13.1c.35-.21.8.03.8.44A9 9 0 1 1 12.76 3z"/>
</svg>
</button>
</div>
<!-- Measure Distance (Ruler) -->
<div class="map-control-group">
<button id="measure-toggle" class="map-control-btn" title="خط‌کش - اندازه‌گیری فاصله">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="6" cy="12" r="2"/>
<circle cx="18" cy="12" r="2"/>
<line x1="8" y1="12" x2="16" y2="12"/>
</svg>
</button>
<button id="route-toggle" class="map-control-btn" title="مسیریابی روی جاده (فقط ایران)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="6" cy="6" r="2"/><circle cx="18" cy="18" r="2"/>
<path d="M6 8v4l6 4 6-4V8"/>
</svg>
</button>
</div>
<!-- Location Controls -->
<div class="map-control-group">
<button id="locate-me" class="map-control-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"/>
<circle cx="12" cy="9" r="2.5"/>
</svg>
</button>
<button id="reset-view" class="map-control-btn" title="بازگشت به نمای پیش‌فرض نقشه ایران">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
</svg>
</button>
</div>
</div>
<!-- Measure Distance Panel (shown when measure mode is on) -->
<div id="measure-panel" class="map-info-panel measure-panel" style="display: none;">
<div class="info-content">
<h6 class="mb-2">خط‌کش – اندازه‌گیری فاصله</h6>
<p id="measure-hint" class="small text-muted mb-2">روی نقشه کلیک کنید تا نقاط را قرار دهید (حداقل دو نقطه).</p>
<div id="measure-distance" class="info-item mb-2" style="display: none;">
<span class="info-label">فاصله کل:</span>
<span id="measure-distance-value" class="fw-bold">–</span>
</div>
<button id="measure-clear" type="button" class="btn btn-sm btn-outline-secondary w-100" style="display: none;">پاک کردن و اندازه‌گیری مجدد</button>
</div>
</div>
<!-- Route Panel (shown when route mode is on) -->
<div id="route-panel" class="map-info-panel measure-panel" style="display: none;">
<div class="info-content">
<h6 class="mb-2">مسیریابی روی جاده</h6>
<p id="route-hint" class="small text-muted mb-2">مبدأ: روی نقشه کلیک کنید.</p>
<div id="route-result" class="mb-2" style="display: none;">
<div class="info-item mb-1">
<span class="info-label">فاصله:</span>
<span id="route-distance" class="fw-bold">–</span>
</div>
<div class="info-item mb-2">
<span class="info-label">زمان تقریبی:</span>
<span id="route-duration" class="fw-bold">–</span>
</div>
</div>
<div id="route-fallback-msg" class="small text-warning mb-2" style="display: none;">مسیر جاده‌ای یافت نشد؛ خط تقریبی (مبدأ–مقصد) نمایش داده می‌شود.</div>
<div id="route-error" class="small text-danger mb-2" style="display: none;"></div>
<button id="route-clear" type="button" class="btn btn-sm btn-outline-secondary w-100" style="display: none;">پاک کردن مسیر</button>
</div>
</div>
<!-- Map Info Panel -->
<div class="map-info-panel">
<div class="info-content">
<h6 class="mb-2">اطلاعات نقشه آنلاین</h6>
<div class="info-item">
<span class="info-label">مختصات:</span>
<span id="coordinates">در حال بارگذاری نقشه...</span>
</div>
<div class="info-item">
<span class="info-label">کیفیت نقشه:</span>
<span id="current-layer">استاندارد</span>
</div>
<div class="info-item">
<span class="info-label">حالت:</span>
<span id="current-theme">روشن</span>
</div>
</div>
</div>
<!-- Full Screen Toggle -->
<div class="fullscreen-toggle">
<button id="fullscreen-btn" class="map-control-btn" title="نمایش تمام صفحه">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>
</button>
</div>
<!-- Map Container -->
<div id="fullscreen-map"></div>
</div>
{% endblock %}
{% block leaflet_css %}
{{ parent() }}
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
}
#fullscreen-map-container {
width: 100vw;
height: 100vh;
position: relative;
}
#fullscreen-map {
width: 100%;
height: 100%;
z-index: 1;
}
/* Hide Leaflet's built-in zoom controls */
.leaflet-control-zoom {
display: none !important;
}
/* Hide Leaflet attribution */
.leaflet-control-attribution {
display: none !important;
}
.back-home-btn {
position: absolute;
top: 20px;
left: 20px;
z-index: 1000;
}
.map-controls-overlay {
position: absolute;
top: 20px;
right: 20px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 10px;
}
.map-control-group {
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 8px;
display: flex;
flex-direction: column;
gap: 5px;
}
.map-control-btn {
background: white;
border: 1px solid #ddd;
border-radius: 6px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
color: #333;
text-decoration: none;
}
.map-control-btn:hover {
background: #f8f9fa;
border-color: #007bff;
transform: translateY(-1px);
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
color: #333;
text-decoration: none;
}
.map-control-btn.active {
background: #007bff;
color: white;
border-color: #007bff;
}
.map-control-btn svg {
width: 16px;
height: 16px;
}
.map-info-panel {
position: absolute;
bottom: 20px;
left: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 15px;
z-index: 1000;
min-width: 200px;
}
.measure-panel {
bottom: auto;
top: 50%;
transform: translateY(-50%);
left: 20px;
}
.info-content h6 {
color: #333;
font-weight: bold;
margin-bottom: 10px;
font-size: 14px;
}
.info-item {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 12px;
}
.info-label {
font-weight: bold;
color: #666;
}
.fullscreen-toggle {
position: absolute;
top: 20px;
left: 80px;
z-index: 1000;
}
/* Responsive Design */
@media (max-width: 768px) {
.back-home-btn {
top: 10px;
left: 10px;
}
.map-controls-overlay {
top: 10px;
right: 10px;
}
.map-info-panel {
bottom: 10px;
left: 10px;
padding: 10px;
min-width: 180px;
}
.fullscreen-toggle {
top: 10px;
left: 60px;
}
.map-control-btn {
width: 35px;
height: 35px;
}
.map-control-btn svg {
width: 14px;
height: 14px;
}
}
/* Hide navbar and footer for fullscreen map */
.navbar, .footer {
display: none !important;
}
main {
margin-top: 0 !important;
}
/* Leaflet Popup Styles with Vazirmatn Font */
.leaflet-popup-content-wrapper {
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif !important;
direction: rtl !important;
text-align: right !important;
border-radius: 8px !important;
box-shadow: 0 4px 16px rgba(0,0,0,0.15) !important;
border: none !important;
}
.leaflet-popup-content {
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif !important;
direction: rtl !important;
text-align: right !important;
margin: 0 !important;
padding: 0 !important;
}
.leaflet-popup-tip {
background: white !important;
}
/* Custom Marker Styles */
.custom-marker {
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif !important;
}
.user-location-marker {
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif !important;
}
/* Additional Leaflet Popup Styles */
.leaflet-popup-content-wrapper {
border-radius: 8px !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
}
.leaflet-popup-content {
line-height: 1.4 !important;
}
.leaflet-popup-close-button {
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif !important;
font-size: 16px !important;
font-weight: 600 !important;
color: #999 !important;
right: 6px !important;
left: auto !important;
top: 6px !important;
width: 20px !important;
height: 20px !important;
line-height: 20px !important;
text-align: center !important;
background: rgba(255,255,255,0.9) !important;
border-radius: 50% !important;
border: none !important;
transition: all 0.2s ease !important;
}
.leaflet-popup-close-button:hover {
background: rgba(255,255,255,1) !important;
color: #333 !important;
transform: scale(1.1) !important;
}
.leaflet-tooltip {
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif !important;
direction: rtl !important;
text-align: right !important;
background: rgba(0,0,0,0.8) !important;
color: white !important;
border: none !important;
border-radius: 4px !important;
padding: 4px 8px !important;
font-size: 12px !important;
font-weight: 500 !important;
}
/* Layer Switcher Menu (unified layers + APIs) */
.layer-switcher-menu {
position: absolute;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
padding: 10px;
z-index: 1002;
min-width: 240px;
max-width: 300px;
max-height: 85vh;
overflow-y: auto;
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.layer-switcher-menu .menu-section {
margin-bottom: 8px;
}
.layer-switcher-menu .menu-section:last-child {
margin-bottom: 0;
}
.menu-section-title {
font-size: 12px;
font-weight: 600;
color: #666;
padding: 6px 12px 4px;
text-align: right;
direction: rtl;
border-bottom: 1px solid #eee;
margin-bottom: 4px;
}
/* Google Earth Menu Styles */
.google-earth-menu {
position: absolute;
top: 20px;
right: 20px;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
padding: 10px;
z-index: 1001;
min-width: 220px;
max-width: 280px;
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
}
.menu-header {
font-weight: bold;
font-size: 14px;
padding: 8px 12px;
border-bottom: 1px solid #eee;
margin-bottom: 5px;
color: #333;
text-align: right;
}
.menu-item {
width: 100%;
padding: 10px 12px;
border: none;
background: white;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
text-align: right;
direction: rtl;
font-family: 'Vazirmatn', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-size: 13px;
color: #333;
transition: all 0.2s ease;
margin-bottom: 3px;
}
.menu-item:hover {
background: #f8f9fa;
transform: translateX(-3px);
}
.menu-item.active {
background: #007bff;
color: white;
}
.menu-item.menu-item-back {
border-top: 1px solid #eee;
margin-top: 5px;
padding-top: 12px;
font-weight: 500;
}
.menu-icon {
font-size: 16px;
width: 24px;
text-align: center;
}
@media (max-width: 768px) {
.layer-switcher-menu {
top: 10px;
right: 10px;
min-width: 200px;
max-width: 260px;
max-height: 80vh;
}
.google-earth-menu {
top: 10px;
right: 10px;
min-width: 200px;
max-width: 250px;
}
}
</style>
{% endblock %}
{% block javascripts %}
<script src="{{ asset('assets/leaflet/leaflet.js') }}"></script>
<script src="{{ asset('assets/js/Leaflet.VectorGrid.bundled.js') }}"></script>
<script src="{{ asset('js/search.js') }}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const API_BASE = '{{ app.request.schemeAndHttpHost }}';
// Initialize map
const map = L.map('fullscreen-map', {
zoomControl: false // Disable built-in zoom controls
}).setView([35.6892, 51.3890], 10); // Tehran coordinates
// OpenStreetMap tile layer (default when no saved settings)
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19
});
// Define raster tile layers (internal API - same origin)
const rasterTileLayers = {
hot: L.tileLayer(API_BASE + '/hot/{z}/{x}/{y}.png', {
attribution: '',
maxZoom: 18,
}),
ret: L.tileLayer(API_BASE + '/ret/{z}/{x}/{y}.png', {
attribution: '',
maxZoom: 18,
tileSize: 512,
zoomOffset: -1
}),
dark: L.tileLayer(API_BASE + '/dark/{z}/{x}/{y}.png', {
attribution: '',
maxZoom: 18
}),
dark_retina: L.tileLayer(API_BASE + '/dark-retina/{z}/{x}/{y}.png', {
attribution: '',
maxZoom: 18,
tileSize: 512,
zoomOffset: -1
})
};
// Define Google Earth tile layers (internal API)
const googleEarthLayers = {
satellite: L.tileLayer(API_BASE + '/api/google-earth/satellite/{z}/{x}/{y}.png', {
attribution: '© Google Earth via Memaps.ir',
maxZoom: 20,
crossOrigin: true
}),
map: L.tileLayer(API_BASE + '/api/google-earth/map/{z}/{x}/{y}.png', {
attribution: '© Google Earth via Memaps.ir',
maxZoom: 20,
crossOrigin: true
}),
hybrid: L.tileLayer(API_BASE + '/api/google-earth/hybrid/{z}/{x}/{y}.png', {
attribution: '© Google Earth via Memaps.ir',
maxZoom: 20,
crossOrigin: true
}),
terrain: L.tileLayer(API_BASE + '/api/google-earth/terrain/{z}/{x}/{y}.png', {
attribution: '© Google Earth via Memaps.ir',
maxZoom: 20,
crossOrigin: true
}),
roadmap: L.tileLayer(API_BASE + '/api/google-earth/roadmap/{z}/{x}/{y}.png', {
attribution: '© Google Earth via Memaps.ir',
maxZoom: 20,
crossOrigin: true
})
};
const bingLayers = {
road: L.tileLayer(API_BASE + '/api/bing/road/{z}/{x}/{y}.png', {
attribution: '© Bing via Memaps.ir',
maxZoom: 23,
crossOrigin: true
}),
aerial: L.tileLayer(API_BASE + '/api/bing/aerial/{z}/{x}/{y}.jpg', {
attribution: '© Bing via Memaps.ir',
maxZoom: 23,
crossOrigin: true
}),
hybrid: L.tileLayer(API_BASE + '/api/bing/hybrid/{z}/{x}/{y}.jpg', {
attribution: '© Bing via Memaps.ir',
maxZoom: 23,
crossOrigin: true
})
};
// لایه‌های وکتوری MVT (به‌صورت overlay روی هر پایه)
const vectorOverlayLayers = {};
const vectorOverlaysOn = { roads: false, buildings: false, points: false };
if (typeof L !== 'undefined' && L.vectorGrid && L.vectorGrid.protobuf) {
vectorOverlayLayers.roads = L.vectorGrid.protobuf(API_BASE + '/api/vector/roads/{z}/{x}/{y}.pbf', {
vectorTileLayerStyles: {
roads: { color: '#555', weight: 1.5, opacity: 0.9 }
},
interactive: true,
getFeatureId: function(f) { return f.properties.osm_id; }
});
vectorOverlayLayers.buildings = L.vectorGrid.protobuf(API_BASE + '/api/vector/buildings/{z}/{x}/{y}.pbf', {
vectorTileLayerStyles: {
buildings: { color: '#888', fillColor: '#ddd', fillOpacity: 0.4, weight: 0.8 }
},
interactive: true,
getFeatureId: function(f) { return f.properties.osm_id; }
});
vectorOverlayLayers.points = L.vectorGrid.protobuf(API_BASE + '/api/vector/points/{z}/{x}/{y}.pbf', {
vectorTileLayerStyles: {
points: { radius: 4, color: '#c00', fillColor: '#f66', fillOpacity: 0.8, weight: 1 }
},
interactive: true,
getFeatureId: function(f) { return f.properties.osm_id; }
});
}
// Storage key for map settings
const MAP_SETTINGS_KEY = 'memaps_map_settings';
// State: theme, quality, and layer type (loaded from localStorage or defaults to raster/internal API)
let currentTheme = 'light';
let currentQuality = 'standard';
let layerType = 'raster'; // 'osm' | 'raster' | 'google-earth' | 'bing' — default: raster (internal API)
let currentGoogleEarthLayer = 'satellite';
let currentBingLayer = 'road';
let activeLayer = null;
function getLayerKey() {
if (layerType === 'osm') return 'standard';
if (layerType === 'google-earth') return currentGoogleEarthLayer;
if (layerType === 'bing') return currentBingLayer;
if (currentTheme === 'light') return currentQuality === 'standard' ? 'hot' : 'ret';
return currentQuality === 'standard' ? 'dark' : 'dark_retina';
}
function getMapSettings() {
return {
layerType: layerType,
currentTheme: currentTheme,
currentQuality: currentQuality,
currentGoogleEarthLayer: currentGoogleEarthLayer,
currentBingLayer: currentBingLayer,
vectorOverlaysOn: vectorOverlaysOn,
center: { lat: map.getCenter().lat, lng: map.getCenter().lng },
zoom: map.getZoom()
};
}
function saveMapSettings() {
try {
localStorage.setItem(MAP_SETTINGS_KEY, JSON.stringify(getMapSettings()));
} catch (e) {}
}
function loadMapSettings() {
try {
const raw = localStorage.getItem(MAP_SETTINGS_KEY);
if (!raw) return false;
const s = JSON.parse(raw);
const validTypes = ['osm', 'raster', 'google-earth', 'bing'];
if (!s.layerType || !validTypes.includes(s.layerType)) return false;
layerType = s.layerType;
if (s.currentTheme) currentTheme = s.currentTheme;
if (s.currentQuality) currentQuality = s.currentQuality;
if (s.currentGoogleEarthLayer) currentGoogleEarthLayer = s.currentGoogleEarthLayer;
if (s.currentBingLayer) currentBingLayer = s.currentBingLayer;
if (s.vectorOverlaysOn && typeof s.vectorOverlaysOn === 'object') {
vectorOverlaysOn.roads = !!s.vectorOverlaysOn.roads;
vectorOverlaysOn.buildings = !!s.vectorOverlaysOn.buildings;
vectorOverlaysOn.points = !!s.vectorOverlaysOn.points;
}
if (s.center && typeof s.center.lat === 'number' && typeof s.center.lng === 'number') {
map.setView([s.center.lat, s.center.lng], typeof s.zoom === 'number' ? s.zoom : 10);
}
return true;
} catch (e) {
return false;
}
}
function applyVectorOverlays() {
['roads', 'buildings', 'points'].forEach(function(overlay) {
if (!vectorOverlayLayers[overlay]) return;
if (vectorOverlaysOn[overlay]) {
if (!map.hasLayer(vectorOverlayLayers[overlay])) {
map.addLayer(vectorOverlayLayers[overlay]);
}
} else {
if (map.hasLayer(vectorOverlayLayers[overlay])) {
map.removeLayer(vectorOverlayLayers[overlay]);
}
}
});
const menuEl = document.getElementById('layer-switcher-menu');
if (menuEl) {
menuEl.querySelectorAll('.menu-item-overlay[data-overlay]').forEach(function(btn) {
const overlay = btn.getAttribute('data-overlay');
btn.classList.toggle('active', !!vectorOverlaysOn[overlay]);
});
}
}
function applyLayer() {
const key = getLayerKey();
if (activeLayer) {
map.removeLayer(activeLayer);
}
if (layerType === 'osm') {
osmLayer.addTo(map);
activeLayer = osmLayer;
} else if (layerType === 'google-earth') {
googleEarthLayers[key].addTo(map);
activeLayer = googleEarthLayers[key];
} else if (layerType === 'bing') {
bingLayers[key].addTo(map);
activeLayer = bingLayers[key];
} else {
rasterTileLayers[key].addTo(map);
activeLayer = rasterTileLayers[key];
}
saveMapSettings();
}
// Load saved settings or use raster (internal API) as default
if (!loadMapSettings()) {
layerType = 'raster';
}
applyLayer();
applyVectorOverlays();
document.getElementById('layer-toggle').classList.toggle('active', currentQuality === 'retina');
document.getElementById('theme-toggle').classList.toggle('active', currentTheme === 'dark');
// Update info panel
function updateInfo() {
const center = map.getCenter();
document.getElementById('coordinates').textContent =
`${center.lat.toFixed(4)}, ${center.lng.toFixed(4)}`;
let layerText = '';
if (layerType === 'osm') {
layerText = 'OpenStreetMap';
} else if (layerType === 'google-earth') {
const layerNames = {
'satellite': 'ماهواره‌ای',
'map': 'نقشه',
'hybrid': 'ترکیبی',
'terrain': 'زمین‌شناسی',
'roadmap': 'نقشه جاده'
};
layerText = `Google Earth - ${layerNames[currentGoogleEarthLayer]}`;
} else if (layerType === 'bing') {
const bingNames = { 'road': 'جاده', 'aerial': 'ماهواره', 'hybrid': 'ترکیبی' };
layerText = `Bing - ${bingNames[currentBingLayer]}`;
} else {
layerText = currentQuality === 'standard' ? 'استاندارد' : 'رتینا';
}
document.getElementById('current-layer').textContent = layerText;
const themeText = currentTheme === 'light' ? 'روشن' : 'تاریک';
const themeEl = document.getElementById('current-theme');
if (themeEl) themeEl.textContent = themeText;
}
// Map event listeners (save center/zoom for next visit)
map.on('moveend', function() { updateInfo(); saveMapSettings(); });
map.on('zoomend', function() { updateInfo(); saveMapSettings(); });
// Zoom controls
document.getElementById('zoom-in').addEventListener('click', function() {
map.zoomIn();
});
document.getElementById('zoom-out').addEventListener('click', function() {
map.zoomOut();
});
// Layer quality toggle (Standard/Retina) - only for raster layers
document.getElementById('layer-toggle').addEventListener('click', function() {
if (layerType === 'raster') {
currentQuality = currentQuality === 'standard' ? 'retina' : 'standard';
this.classList.toggle('active', currentQuality === 'retina');
applyLayer();
updateInfo();
}
});
// Theme toggle (Light/Dark) - only for raster layers
document.getElementById('theme-toggle').addEventListener('click', function() {
if (layerType === 'raster') {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
this.classList.toggle('active', currentTheme === 'dark');
applyLayer();
updateInfo();
}
});
// Google Earth toggle button
const googleEarthToggleBtn = document.getElementById('google-earth-toggle');
const googleEarthMenu = document.getElementById('google-earth-menu');
googleEarthToggleBtn.addEventListener('click', function() {
if (layerType === 'raster') {
// Switch to Google Earth mode
layerType = 'google-earth';
this.classList.add('active');
googleEarthMenu.style.display = 'block';
// Activate satellite layer by default
currentGoogleEarthLayer = 'satellite';
menuItems.forEach(mi => {
if (mi.getAttribute('data-layer') === 'satellite') {
mi.classList.add('active');
} else {
mi.classList.remove('active');
}
});
applyLayer();
updateInfo();
} else {
// Toggle menu visibility
googleEarthMenu.style.display = googleEarthMenu.style.display === 'none' ? 'block' : 'none';
}
});
// Google Earth menu items
const menuItems = googleEarthMenu.querySelectorAll('.menu-item[data-layer]');
menuItems.forEach(item => {
item.addEventListener('click', function() {
const layer = this.getAttribute('data-layer');
currentGoogleEarthLayer = layer;
// Update active state
menuItems.forEach(mi => mi.classList.remove('active'));
this.classList.add('active');
applyLayer();
updateInfo();
});
});
// Back to raster button
const backButton = googleEarthMenu.querySelector('.menu-item-back');
backButton.addEventListener('click', function() {
layerType = 'raster';
googleEarthToggleBtn.classList.remove('active');
googleEarthMenu.style.display = 'none';
applyLayer();
updateInfo();
});
// ---- Layer Switcher (switch between all layers/APIs) ----
const layerSwitchBtn = document.getElementById('layer-switch');
const layerSwitcherMenu = document.getElementById('layer-switcher-menu');
function syncLayerSwitcherActiveState() {
const key = getLayerKey();
const source = layerType === 'osm' ? 'osm' : (layerType === 'google-earth' ? 'google-earth' : (layerType === 'bing' ? 'bing' : 'raster'));
layerSwitcherMenu.querySelectorAll('.menu-item[data-source][data-layer]').forEach(function(btn) {
const isActive = btn.getAttribute('data-source') === source && btn.getAttribute('data-layer') === key;
btn.classList.toggle('active', isActive);
});
}
layerSwitchBtn.addEventListener('click', function() {
const isOpen = layerSwitcherMenu.style.display !== 'none';
layerSwitcherMenu.style.display = isOpen ? 'none' : 'block';
googleEarthMenu.style.display = 'none';
if (!isOpen) {
syncLayerSwitcherActiveState();
}
});
layerSwitcherMenu.querySelectorAll('.menu-item[data-source][data-layer]').forEach(function(btn) {
btn.addEventListener('click', function() {
const source = this.getAttribute('data-source');
const layer = this.getAttribute('data-layer');
if (source === 'osm') {
layerType = 'osm';
googleEarthToggleBtn.classList.remove('active');
googleEarthMenu.style.display = 'none';
} else if (source === 'raster') {
layerType = 'raster';
googleEarthToggleBtn.classList.remove('active');
googleEarthMenu.style.display = 'none';
if (layer === 'hot') { currentTheme = 'light'; currentQuality = 'standard'; }
else if (layer === 'ret') { currentTheme = 'light'; currentQuality = 'retina'; }
else if (layer === 'dark') { currentTheme = 'dark'; currentQuality = 'standard'; }
else if (layer === 'dark_retina') { currentTheme = 'dark'; currentQuality = 'retina'; }
} else if (source === 'bing') {
layerType = 'bing';
currentBingLayer = layer;
googleEarthToggleBtn.classList.remove('active');
googleEarthMenu.style.display = 'none';
} else {
layerType = 'google-earth';
currentGoogleEarthLayer = layer;
googleEarthToggleBtn.classList.add('active');
menuItems.forEach(function(mi) {
mi.classList.toggle('active', mi.getAttribute('data-layer') === layer);
});
}
document.getElementById('layer-toggle').classList.toggle('active', currentQuality === 'retina');
document.getElementById('theme-toggle').classList.toggle('active', currentTheme === 'dark');
applyLayer();
updateInfo();
syncLayerSwitcherActiveState();
layerSwitcherMenu.style.display = 'none';
});
});
// لایه‌های وکتوری (overlay): کلیک = روشن/خاموش
layerSwitcherMenu.querySelectorAll('.menu-item-overlay[data-overlay]').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const overlay = this.getAttribute('data-overlay');
if (!vectorOverlayLayers[overlay]) return;
vectorOverlaysOn[overlay] = !vectorOverlaysOn[overlay];
this.classList.toggle('active', vectorOverlaysOn[overlay]);
if (vectorOverlaysOn[overlay]) {
map.addLayer(vectorOverlayLayers[overlay]);
} else {
map.removeLayer(vectorOverlayLayers[overlay]);
}
saveMapSettings();
});
});
document.addEventListener('click', function(event) {
if (!layerSwitcherMenu.contains(event.target) && !layerSwitchBtn.contains(event.target)) {
layerSwitcherMenu.style.display = 'none';
}
});
// Close menu when clicking outside
document.addEventListener('click', function(event) {
if (layerType === 'google-earth' &&
!googleEarthMenu.contains(event.target) &&
!googleEarthToggleBtn.contains(event.target)) {
googleEarthMenu.style.display = 'none';
}
});
// Location control
document.getElementById('locate-me').addEventListener('click', function() {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position) {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
map.setView([lat, lng], 15);
// Add location marker
L.marker([lat, lng]).addTo(map)
.bindPopup('<div style="font-family: \'Vazirmatn\', -apple-system, BlinkMacSystemFont, \'Segoe UI\', \'Roboto\', sans-serif; text-align: right; direction: rtl; font-weight: 500;">موقعیت شما</div>')
.openPopup();
}, function(error) {
alert('خطا در دریافت موقعیت: ' + error.message);
});
} else {
alert('مرورگر شما از GPS پشتیبانی نمی‌کند');
}
});
// Reset view
document.getElementById('reset-view').addEventListener('click', function() {
map.setView([35.6892, 51.3890], 10);
});
// ----- خط‌کش (اندازه‌گیری فاصله) -----
let measureMode = false;
let measurePoints = [];
let measureLayer = L.layerGroup();
let measureLine = null;
let measureClickHandler = null;
let routeMode = false;
let routeStart = null;
let routeEnd = null;
let routeLayer = L.layerGroup();
let routePolyline = null;
let routeClickHandler = null;
function formatMeasureDistance(meters) {
if (meters >= 1000) {
return (meters / 1000).toFixed(2) + ' کیلومتر';
}
return Math.round(meters) + ' متر';
}
function getMeasureTotalDistance() {
if (measurePoints.length < 2) return 0;
let total = 0;
for (let i = 0; i < measurePoints.length - 1; i++) {
total += measurePoints[i].distanceTo(measurePoints[i + 1]);
}
return total;
}
function updateMeasureDisplay() {
const panel = document.getElementById('measure-panel');
const distanceEl = document.getElementById('measure-distance');
const valueEl = document.getElementById('measure-distance-value');
const clearBtn = document.getElementById('measure-clear');
if (measurePoints.length >= 2) {
const totalM = getMeasureTotalDistance();
valueEl.textContent = formatMeasureDistance(totalM);
distanceEl.style.display = 'flex';
clearBtn.style.display = 'block';
} else {
distanceEl.style.display = 'none';
clearBtn.style.display = 'none';
}
}
function clearMeasure() {
measurePoints = [];
if (measureLine) {
measureLayer.removeLayer(measureLine);
measureLine = null;
}
measureLayer.clearLayers();
if (map.hasLayer(measureLayer)) {
map.removeLayer(measureLayer);
}
updateMeasureDisplay();
}
function exitMeasureMode() {
measureMode = false;
document.getElementById('measure-toggle').classList.remove('active');
document.getElementById('measure-panel').style.display = 'none';
document.getElementById('fullscreen-map').style.cursor = '';
if (measureClickHandler) {
map.off('click', measureClickHandler);
measureClickHandler = null;
}
clearMeasure();
}
document.getElementById('measure-toggle').addEventListener('click', function() {
if (measureMode) {
exitMeasureMode();
return;
}
if (routeMode) exitRouteMode();
measureMode = true;
this.classList.add('active');
document.getElementById('measure-panel').style.display = 'block';
document.getElementById('fullscreen-map').style.cursor = 'crosshair';
clearMeasure();
measureClickHandler = function(e) {
measurePoints.push(e.latlng);
L.circleMarker(e.latlng, {
radius: 6,
fillColor: '#3388ff',
color: '#fff',
weight: 2,
fillOpacity: 0.9
}).addTo(measureLayer);
if (measurePoints.length >= 2) {
if (measureLine) {
measureLayer.removeLayer(measureLine);
}
measureLine = L.polyline(measurePoints, {
color: '#3388ff',
weight: 3,
opacity: 0.8
});
measureLayer.addLayer(measureLine);
}
if (!map.hasLayer(measureLayer)) {
map.addLayer(measureLayer);
}
updateMeasureDisplay();
};
map.on('click', measureClickHandler);
});
document.getElementById('measure-clear').addEventListener('click', function() {
clearMeasure();
});
function clearRoute() {
routeStart = null;
routeEnd = null;
if (routePolyline) {
routeLayer.removeLayer(routePolyline);
routePolyline = null;
}
routeLayer.clearLayers();
if (map.hasLayer(routeLayer)) map.removeLayer(routeLayer);
document.getElementById('route-result').style.display = 'none';
document.getElementById('route-fallback-msg').style.display = 'none';
document.getElementById('route-error').style.display = 'none';
document.getElementById('route-error').textContent = '';
document.getElementById('route-clear').style.display = 'none';
document.getElementById('route-hint').textContent = 'مبدأ: روی نقشه کلیک کنید.';
}
function exitRouteMode() {
routeMode = false;
document.getElementById('route-toggle').classList.remove('active');
document.getElementById('route-panel').style.display = 'none';
document.getElementById('fullscreen-map').style.cursor = '';
if (routeClickHandler) {
map.off('click', routeClickHandler);
routeClickHandler = null;
}
clearRoute();
}
document.getElementById('route-toggle').addEventListener('click', function() {
if (routeMode) {
exitRouteMode();
return;
}
if (measureMode) exitMeasureMode();
routeMode = true;
this.classList.add('active');
document.getElementById('route-panel').style.display = 'block';
document.getElementById('fullscreen-map').style.cursor = 'crosshair';
clearRoute();
routeClickHandler = function(e) {
const lat = e.latlng.lat;
const lng = e.latlng.lng;
if (routeStart === null) {
routeStart = { lat: lat, lng: lng };
L.circleMarker(e.latlng, { radius: 8, fillColor: '#28a745', color: '#fff', weight: 2, fillOpacity: 0.9 }).addTo(routeLayer);
document.getElementById('route-hint').textContent = 'مقصد: روی نقشه کلیک کنید.';
if (!map.hasLayer(routeLayer)) map.addLayer(routeLayer);
return;
}
routeEnd = { lat: lat, lng: lng };
L.circleMarker(e.latlng, { radius: 8, fillColor: '#dc3545', color: '#fff', weight: 2, fillOpacity: 0.9 }).addTo(routeLayer);
map.off('click', routeClickHandler);
routeClickHandler = null;
document.getElementById('route-hint').textContent = 'در حال محاسبه مسیر...';
document.getElementById('fullscreen-map').style.cursor = 'wait';
const url = API_BASE + '/api/route?from=' + encodeURIComponent(routeStart.lng + ',' + routeStart.lat) + '&to=' + encodeURIComponent(routeEnd.lng + ',' + routeEnd.lat);
fetch(url)
.then(function(r) { return r.json().then(function(d) { return { ok: r.ok, data: d }; }); })
.then(function(o) {
document.getElementById('fullscreen-map').style.cursor = '';
if (!o.ok) {
document.getElementById('route-hint').textContent = 'مبدأ: روی نقشه کلیک کنید.';
document.getElementById('route-error').textContent = o.data.error || 'خطا در دریافت مسیر';
document.getElementById('route-error').style.display = 'block';
document.getElementById('route-fallback-msg').style.display = 'none';
document.getElementById('route-clear').style.display = 'block';
return;
}
document.getElementById('route-error').style.display = 'none';
var isFallback = !!o.data.fallback;
document.getElementById('route-fallback-msg').style.display = isFallback ? 'block' : 'none';
var coords = o.data.coordinates || [];
if (coords.length >= 2) {
var latlngs = coords.map(function(c) { return [c[1], c[0]]; });
if (routePolyline) routeLayer.removeLayer(routePolyline);
routePolyline = L.polyline(latlngs, { color: '#0d6efd', weight: 5, opacity: 0.9 });
routeLayer.addLayer(routePolyline);
map.fitBounds(routePolyline.getBounds(), { padding: [30, 30] });
}
document.getElementById('route-distance').textContent = (o.data.distance_m / 1000).toFixed(2) + ' کیلومتر';
var sec = o.data.duration_s;
var min = Math.floor(sec / 60);
document.getElementById('route-duration').textContent = (min > 0 ? min + ' دقیقه ' : '') + Math.round(sec % 60) + ' ثانیه';
document.getElementById('route-result').style.display = 'block';
document.getElementById('route-hint').textContent = 'برای مسیر جدید دوباره دو نقطه انتخاب کنید.';
document.getElementById('route-clear').style.display = 'block';
})
.catch(function() {
document.getElementById('fullscreen-map').style.cursor = '';
document.getElementById('route-hint').textContent = 'مبدأ: روی نقشه کلیک کنید.';
document.getElementById('route-error').textContent = 'خطا در ارتباط با سرور';
document.getElementById('route-error').style.display = 'block';
document.getElementById('route-clear').style.display = 'block';
});
};
map.on('click', routeClickHandler);
});
document.getElementById('route-clear').addEventListener('click', function() {
clearRoute();
if (routeMode && !routeClickHandler) {
document.getElementById('route-hint').textContent = 'مبدأ: روی نقشه کلیک کنید.';
routeClickHandler = function(e) {
const lat = e.latlng.lat, lng = e.latlng.lng;
if (routeStart === null) {
routeStart = { lat: lat, lng: lng };
L.circleMarker(e.latlng, { radius: 8, fillColor: '#28a745', color: '#fff', weight: 2, fillOpacity: 0.9 }).addTo(routeLayer);
document.getElementById('route-hint').textContent = 'مقصد: روی نقشه کلیک کنید.';
if (!map.hasLayer(routeLayer)) map.addLayer(routeLayer);
return;
}
routeEnd = { lat: lat, lng: lng };
L.circleMarker(e.latlng, { radius: 8, fillColor: '#dc3545', color: '#fff', weight: 2, fillOpacity: 0.9 }).addTo(routeLayer);
map.off('click', routeClickHandler);
routeClickHandler = null;
document.getElementById('route-hint').textContent = 'در حال محاسبه مسیر...';
document.getElementById('fullscreen-map').style.cursor = 'wait';
var url = API_BASE + '/api/route?from=' + encodeURIComponent(routeStart.lng + ',' + routeStart.lat) + '&to=' + encodeURIComponent(routeEnd.lng + ',' + routeEnd.lat);
fetch(url).then(function(r){ return r.json().then(function(d){ return {ok:r.ok, data:d}; }); }).then(function(o){
document.getElementById('fullscreen-map').style.cursor = '';
if (!o.ok) {
document.getElementById('route-hint').textContent = 'مبدأ: روی نقشه کلیک کنید.';
document.getElementById('route-error').textContent = o.data.error || 'خطا در دریافت مسیر';
document.getElementById('route-error').style.display = 'block';
document.getElementById('route-fallback-msg').style.display = 'none';
document.getElementById('route-clear').style.display = 'block';
routeStart = null; routeEnd = null;
return;
}
document.getElementById('route-error').style.display = 'none';
document.getElementById('route-fallback-msg').style.display = o.data.fallback ? 'block' : 'none';
var coords = o.data.coordinates || [];
if (coords.length >= 2) {
var latlngs = coords.map(function(c){ return [c[1], c[0]]; });
if (routePolyline) routeLayer.removeLayer(routePolyline);
routePolyline = L.polyline(latlngs, { color: '#0d6efd', weight: 5, opacity: 0.9 });
routeLayer.addLayer(routePolyline);
map.fitBounds(routePolyline.getBounds(), { padding: [30, 30] });
}
document.getElementById('route-distance').textContent = (o.data.distance_m / 1000).toFixed(2) + ' کیلومتر';
var s = o.data.duration_s, m = Math.floor(s/60);
document.getElementById('route-duration').textContent = (m ? m + ' دقیقه ' : '') + Math.round(s % 60) + ' ثانیه';
document.getElementById('route-result').style.display = 'block';
document.getElementById('route-hint').textContent = 'برای مسیر جدید دوباره دو نقطه انتخاب کنید.';
document.getElementById('route-clear').style.display = 'block';
}).catch(function(){
document.getElementById('fullscreen-map').style.cursor = '';
document.getElementById('route-hint').textContent = 'مبدأ: روی نقشه کلیک کنید.';
document.getElementById('route-error').textContent = 'خطا در ارتباط با سرور';
document.getElementById('route-error').style.display = 'block';
document.getElementById('route-clear').style.display = 'block';
routeStart = null; routeEnd = null;
});
};
map.on('click', routeClickHandler);
}
});
// Fullscreen toggle
document.getElementById('fullscreen-btn').addEventListener('click', function() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
this.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/></svg>';
} else {
document.exitFullscreen();
this.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>';
}
});
// Initialize info panel
updateInfo();
// Initialize search functionality
mapSearch = new MapSearch(map);
// Search toggle button
document.getElementById('search-toggle').addEventListener('click', function() {
const searchPanel = document.querySelector('.search-panel');
if (searchPanel) {
searchPanel.style.display = searchPanel.style.display === 'none' ? 'block' : 'none';
}
});
});
</script>
{% endblock %}