1443 lines
No EOL
67 KiB
Twig
1443 lines
No EOL
67 KiB
Twig
{% 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 %} |