|
|
|
|
|
|
|
""" |
|
وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد |
|
تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد |
|
""" |
|
|
|
import os |
|
import sys |
|
import streamlit as st |
|
import pandas as pd |
|
import numpy as np |
|
import pydeck as pdk |
|
import folium |
|
from folium.plugins import MarkerCluster, HeatMap, MeasureControl |
|
from streamlit_folium import folium_static |
|
import requests |
|
import json |
|
import random |
|
from typing import List, Dict, Any, Tuple, Optional |
|
import tempfile |
|
import base64 |
|
from PIL import Image |
|
from io import BytesIO |
|
|
|
|
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))) |
|
|
|
|
|
from utils.components.header import render_header |
|
from utils.components.credits import render_credits |
|
from utils.helpers import format_number, format_currency, styled_button |
|
|
|
|
|
class InteractiveMap: |
|
"""فئة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد""" |
|
|
|
def __init__(self): |
|
"""تهيئة وحدة الخريطة التفاعلية""" |
|
|
|
self.data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../data/maps")) |
|
os.makedirs(self.data_dir, exist_ok=True) |
|
|
|
|
|
self.mapbox_token = os.environ.get("MAPBOX_TOKEN", "") |
|
self.opentopodata_api = "https://api.opentopodata.org/v1/srtm30m" |
|
|
|
|
|
if 'project_locations' not in st.session_state: |
|
st.session_state.project_locations = [] |
|
|
|
if 'selected_location' not in st.session_state: |
|
st.session_state.selected_location = None |
|
|
|
if 'terrain_data' not in st.session_state: |
|
st.session_state.terrain_data = None |
|
|
|
|
|
self._initialize_sample_projects() |
|
|
|
def render(self): |
|
"""عرض واجهة وحدة الخريطة التفاعلية""" |
|
|
|
render_header("خريطة مواقع المشاريع التفاعلية") |
|
|
|
|
|
tabs = st.tabs([ |
|
"الخريطة التفاعلية", |
|
"عرض التضاريس ثلاثي الأبعاد", |
|
"تحليل المواقع", |
|
"إدارة المواقع" |
|
]) |
|
|
|
|
|
with tabs[0]: |
|
self._render_interactive_map() |
|
|
|
|
|
with tabs[1]: |
|
self._render_3d_terrain() |
|
|
|
|
|
with tabs[2]: |
|
self._render_location_analysis() |
|
|
|
|
|
with tabs[3]: |
|
self._render_location_management() |
|
|
|
|
|
render_credits() |
|
|
|
def _render_interactive_map(self): |
|
"""عرض الخريطة التفاعلية""" |
|
st.markdown(""" |
|
<div class='custom-box info-box'> |
|
<h3>🗺️ الخريطة التفاعلية لمواقع المشاريع</h3> |
|
<p>خريطة تفاعلية تعرض مواقع جميع المشاريع مع إمكانية التكبير والتصغير والتحرك.</p> |
|
<p>يمكنك النقر على أي موقع للحصول على تفاصيل المشروع.</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
search_query = st.text_input("البحث عن موقع أو مشروع", key="map_search") |
|
|
|
|
|
col1, col2, col3, col4 = st.columns(4) |
|
|
|
with col1: |
|
map_style = st.selectbox( |
|
"نمط الخريطة", |
|
options=["OpenStreetMap", "Stamen Terrain", "Stamen Toner", "CartoDB Positron"], |
|
key="map_style" |
|
) |
|
|
|
with col2: |
|
cluster_markers = st.checkbox("تجميع المواقع القريبة", value=True, key="cluster_markers") |
|
|
|
with col3: |
|
show_heatmap = st.checkbox("عرض خريطة حرارية للمواقع", value=False, key="show_heatmap") |
|
|
|
with col4: |
|
show_measurements = st.checkbox("أدوات القياس", value=False, key="show_measurements") |
|
|
|
|
|
if len(st.session_state.project_locations) > 0: |
|
|
|
locations = [] |
|
|
|
|
|
filtered_projects = st.session_state.project_locations |
|
if search_query: |
|
filtered_projects = [ |
|
p for p in filtered_projects |
|
if search_query.lower() in p.get("name", "").lower() or |
|
search_query.lower() in p.get("description", "").lower() or |
|
search_query.lower() in p.get("city", "").lower() |
|
] |
|
|
|
|
|
if search_query: |
|
st.markdown(f"عدد النتائج: {len(filtered_projects)}") |
|
|
|
|
|
heat_data = [] |
|
for project in filtered_projects: |
|
locations.append({ |
|
"lat": project.get("latitude"), |
|
"lon": project.get("longitude"), |
|
"name": project.get("name"), |
|
"description": project.get("description"), |
|
"city": project.get("city"), |
|
"status": project.get("status"), |
|
"project_id": project.get("project_id") |
|
}) |
|
heat_data.append([project.get("latitude"), project.get("longitude"), 1]) |
|
|
|
|
|
if filtered_projects: |
|
center_lat = sum(p.get("latitude", 0) for p in filtered_projects) / len(filtered_projects) |
|
center_lon = sum(p.get("longitude", 0) for p in filtered_projects) / len(filtered_projects) |
|
zoom_level = 6 |
|
else: |
|
|
|
center_lat = 24.7136 |
|
center_lon = 46.6753 |
|
zoom_level = 5 |
|
|
|
|
|
attribution = None |
|
if map_style == "OpenStreetMap": |
|
attribution = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' |
|
elif map_style.startswith("Stamen"): |
|
attribution = 'Map tiles by <a href="http://stamen.com">Stamen Design</a>, under <a href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a href="http://openstreetmap.org">OpenStreetMap</a>, under <a href="http://www.openstreetmap.org/copyright">ODbL</a>.' |
|
elif map_style == "CartoDB Positron": |
|
attribution = '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="https://cartodb.com/attributions">CartoDB</a>' |
|
|
|
|
|
m = folium.Map( |
|
location=[center_lat, center_lon], |
|
zoom_start=zoom_level, |
|
tiles=map_style, |
|
attr=attribution |
|
) |
|
|
|
|
|
if show_measurements: |
|
MeasureControl(position='topright', primary_length_unit='kilometers').add_to(m) |
|
|
|
|
|
if cluster_markers: |
|
|
|
marker_cluster = MarkerCluster(name="تجميع المشاريع").add_to(m) |
|
|
|
|
|
for location in locations: |
|
|
|
popup_html = f""" |
|
<div style='direction: rtl; text-align: right;'> |
|
<h4>{location['name']}</h4> |
|
<p><strong>الوصف:</strong> {location['description']}</p> |
|
<p><strong>المدينة:</strong> {location['city']}</p> |
|
<p><strong>الحالة:</strong> {location['status']}</p> |
|
<p><strong>الإحداثيات:</strong> {location['lat']:.6f}, {location['lon']:.6f}</p> |
|
<button onclick="window.open('/project/{location['project_id']}', '_self')">عرض تفاصيل المشروع</button> |
|
</div> |
|
""" |
|
|
|
|
|
icon_color = 'green' |
|
if location['status'] == 'قيد التنفيذ': |
|
icon_color = 'orange' |
|
elif location['status'] == 'متوقف': |
|
icon_color = 'red' |
|
elif location['status'] == 'مكتمل': |
|
icon_color = 'blue' |
|
|
|
|
|
folium.Marker( |
|
location=[location['lat'], location['lon']], |
|
popup=folium.Popup(popup_html, max_width=300), |
|
tooltip=location['name'], |
|
icon=folium.Icon(color=icon_color, icon='info-sign') |
|
).add_to(marker_cluster) |
|
else: |
|
|
|
for location in locations: |
|
|
|
popup_html = f""" |
|
<div style='direction: rtl; text-align: right;'> |
|
<h4>{location['name']}</h4> |
|
<p><strong>الوصف:</strong> {location['description']}</p> |
|
<p><strong>المدينة:</strong> {location['city']}</p> |
|
<p><strong>الحالة:</strong> {location['status']}</p> |
|
<p><strong>الإحداثيات:</strong> {location['lat']:.6f}, {location['lon']:.6f}</p> |
|
<button onclick="window.open('/project/{location['project_id']}', '_self')">عرض تفاصيل المشروع</button> |
|
</div> |
|
""" |
|
|
|
|
|
icon_color = 'green' |
|
if location['status'] == 'قيد التنفيذ': |
|
icon_color = 'orange' |
|
elif location['status'] == 'متوقف': |
|
icon_color = 'red' |
|
elif location['status'] == 'مكتمل': |
|
icon_color = 'blue' |
|
|
|
|
|
folium.Marker( |
|
location=[location['lat'], location['lon']], |
|
popup=folium.Popup(popup_html, max_width=300), |
|
tooltip=location['name'], |
|
icon=folium.Icon(color=icon_color, icon='info-sign') |
|
).add_to(m) |
|
|
|
|
|
if show_heatmap and heat_data: |
|
HeatMap(heat_data, radius=15).add_to(m) |
|
|
|
|
|
folium.TileLayer('OpenStreetMap').add_to(m) |
|
folium.TileLayer('Stamen Terrain').add_to(m) |
|
folium.TileLayer('Stamen Toner').add_to(m) |
|
folium.TileLayer('CartoDB positron').add_to(m) |
|
folium.TileLayer('CartoDB dark_matter').add_to(m) |
|
|
|
|
|
folium.LayerControl().add_to(m) |
|
|
|
|
|
st_map = folium_static(m, width=1000, height=600) |
|
|
|
|
|
|
|
|
|
|
|
st.markdown("### قائمة المشاريع على الخريطة") |
|
|
|
projects_df = pd.DataFrame(filtered_projects) |
|
|
|
|
|
renamed_columns = { |
|
"name": "اسم المشروع", |
|
"city": "المدينة", |
|
"status": "الحالة", |
|
"description": "الوصف", |
|
"project_id": "معرف المشروع", |
|
"latitude": "خط العرض", |
|
"longitude": "خط الطول" |
|
} |
|
|
|
|
|
display_columns = ["name", "city", "status", "project_id"] |
|
|
|
|
|
display_df = projects_df[display_columns].rename(columns=renamed_columns) |
|
|
|
|
|
st.dataframe(display_df, width=1000, height=400) |
|
|
|
|
|
selected_project_id = st.selectbox( |
|
"اختر مشروعًا لعرض التضاريس ثلاثية الأبعاد", |
|
options=projects_df["project_id"].tolist(), |
|
format_func=lambda x: next((p["name"] for p in filtered_projects if p["project_id"] == x), x), |
|
key="select_project_for_terrain" |
|
) |
|
|
|
|
|
if styled_button("عرض التضاريس", key="show_terrain_btn", type="primary", icon="🏔️"): |
|
|
|
selected_project = next((p for p in filtered_projects if p["project_id"] == selected_project_id), None) |
|
|
|
if selected_project: |
|
|
|
st.session_state.selected_location = { |
|
"latitude": selected_project["latitude"], |
|
"longitude": selected_project["longitude"], |
|
"name": selected_project["name"], |
|
"project_id": selected_project["project_id"] |
|
} |
|
|
|
|
|
try: |
|
terrain_data = self._fetch_terrain_data( |
|
selected_project["latitude"], |
|
selected_project["longitude"] |
|
) |
|
|
|
|
|
st.session_state.terrain_data = terrain_data |
|
|
|
|
|
st.experimental_rerun() |
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") |
|
else: |
|
st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.") |
|
|
|
def _render_3d_terrain(self): |
|
"""عرض التضاريس ثلاثي الأبعاد""" |
|
st.markdown(""" |
|
<div class='custom-box info-box'> |
|
<h3>🏔️ عرض التضاريس ثلاثي الأبعاد</h3> |
|
<p>عرض تفاعلي ثلاثي الأبعاد لتضاريس موقع المشروع المحدد.</p> |
|
<p>يمكنك تدوير وتكبير العرض للحصول على رؤية أفضل للموقع.</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
if st.session_state.selected_location is None: |
|
st.info("يرجى اختيار موقع من تبويب 'الخريطة التفاعلية' أولاً.") |
|
|
|
|
|
st.markdown("### إدخال الإحداثيات يدوياً") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
manual_lat = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="manual_lat") |
|
|
|
with col2: |
|
manual_lon = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="manual_lon") |
|
|
|
if styled_button("عرض التضاريس", key="manual_terrain_btn", type="primary", icon="🏔️"): |
|
try: |
|
|
|
terrain_data = self._fetch_terrain_data(manual_lat, manual_lon) |
|
|
|
|
|
st.session_state.terrain_data = terrain_data |
|
st.session_state.selected_location = { |
|
"latitude": manual_lat, |
|
"longitude": manual_lon, |
|
"name": "موقع مخصص", |
|
"project_id": "custom" |
|
} |
|
|
|
st.success("تم جلب بيانات التضاريس بنجاح!") |
|
st.experimental_rerun() |
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") |
|
|
|
return |
|
|
|
|
|
location = st.session_state.selected_location |
|
st.markdown(f"### عرض تضاريس موقع: {location['name']}") |
|
st.markdown(f"**الإحداثيات:** {location['latitude']:.6f}, {location['longitude']:.6f}") |
|
|
|
|
|
if st.session_state.terrain_data is None: |
|
|
|
try: |
|
terrain_data = self._fetch_terrain_data( |
|
location["latitude"], |
|
location["longitude"] |
|
) |
|
|
|
|
|
st.session_state.terrain_data = terrain_data |
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") |
|
return |
|
|
|
|
|
terrain_data = st.session_state.terrain_data |
|
|
|
|
|
col1, col2, col3 = st.columns(3) |
|
|
|
with col1: |
|
elevation_scale = st.slider( |
|
"مقياس الارتفاع", |
|
min_value=1, |
|
max_value=50, |
|
value=15, |
|
key="elevation_scale" |
|
) |
|
|
|
with col2: |
|
radius = st.slider( |
|
"نطاق العرض (كم)", |
|
min_value=1, |
|
max_value=20, |
|
value=5, |
|
key="terrain_radius" |
|
) |
|
|
|
with col3: |
|
color_scheme = st.selectbox( |
|
"نظام الألوان", |
|
options=["terrain", "elevation", "custom"], |
|
key="color_scheme" |
|
) |
|
|
|
|
|
try: |
|
|
|
terrain_df = pd.DataFrame(terrain_data) |
|
|
|
|
|
cell_size = radius * 100 |
|
|
|
|
|
terrain_layer = pdk.Layer( |
|
"TerrainLayer", |
|
data=None, |
|
elevation_decoder={ |
|
"elevations": "elevation", |
|
"bounds": terrain_df["bounds"].iloc[0] |
|
}, |
|
texture=None, |
|
elevation_data=terrain_df["terrain"].iloc[0], |
|
elevation_scale=elevation_scale, |
|
color_map=self._get_color_map(color_scheme), |
|
wireframe=True, |
|
pickable=True |
|
) |
|
|
|
|
|
point_layer = pdk.Layer( |
|
"ScatterplotLayer", |
|
data=[{ |
|
"position": [location["longitude"], location["latitude"]], |
|
"name": location["name"] |
|
}], |
|
get_position="position", |
|
get_radius=100, |
|
get_fill_color=[255, 0, 0, 200], |
|
pickable=True |
|
) |
|
|
|
|
|
INITIAL_VIEW_STATE = pdk.ViewState( |
|
longitude=location["longitude"], |
|
latitude=location["latitude"], |
|
zoom=12, |
|
max_zoom=20, |
|
pitch=45, |
|
bearing=0 |
|
) |
|
|
|
deck = pdk.Deck( |
|
map_style="mapbox://styles/mapbox/satellite-v9", |
|
initial_view_state=INITIAL_VIEW_STATE, |
|
api_keys={"mapbox": self.mapbox_token} if self.mapbox_token else None, |
|
layers=[terrain_layer, point_layer], |
|
tooltip={ |
|
"html": "<b>{name}</b>", |
|
"style": { |
|
"backgroundColor": "steelblue", |
|
"color": "white" |
|
} |
|
} |
|
) |
|
|
|
|
|
st.pydeck_chart(deck) |
|
|
|
|
|
if "elevation_stats" in terrain_df: |
|
elevation_stats = terrain_df["elevation_stats"].iloc[0] |
|
|
|
st.markdown("### تحليل التضاريس") |
|
|
|
stats_col1, stats_col2, stats_col3, stats_col4 = st.columns(4) |
|
|
|
with stats_col1: |
|
st.metric("أدنى ارتفاع", f"{elevation_stats['min']:.1f} م") |
|
|
|
with stats_col2: |
|
st.metric("أعلى ارتفاع", f"{elevation_stats['max']:.1f} م") |
|
|
|
with stats_col3: |
|
st.metric("متوسط الارتفاع", f"{elevation_stats['mean']:.1f} م") |
|
|
|
with stats_col4: |
|
st.metric("فرق الارتفاع", f"{elevation_stats['range']:.1f} م") |
|
|
|
|
|
if "elevation_profile" in terrain_df: |
|
elevation_profile = terrain_df["elevation_profile"].iloc[0] |
|
|
|
|
|
profile_df = pd.DataFrame(elevation_profile) |
|
|
|
|
|
st.markdown("### مقطع الارتفاع") |
|
|
|
|
|
import plotly.express as px |
|
|
|
fig = px.line( |
|
profile_df, |
|
x="distance", |
|
y="elevation", |
|
title="مقطع الارتفاع عبر الموقع", |
|
labels={"distance": "المسافة (كم)", "elevation": "الارتفاع (م)"} |
|
) |
|
|
|
fig.update_layout( |
|
title_font_size=20, |
|
font_family="Arial", |
|
font_size=14, |
|
height=400 |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
if styled_button("إعادة تحميل بيانات التضاريس", key="reload_terrain", type="primary", icon="🔄"): |
|
|
|
st.session_state.terrain_data = None |
|
st.experimental_rerun() |
|
|
|
with col2: |
|
if styled_button("العودة للخريطة التفاعلية", key="back_to_map", type="secondary", icon="🗺️"): |
|
|
|
st.session_state.selected_location = None |
|
st.session_state.terrain_data = None |
|
st.experimental_rerun() |
|
|
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء عرض التضاريس ثلاثي الأبعاد: {str(e)}") |
|
|
|
def _render_location_analysis(self): |
|
"""عرض تحليل المواقع""" |
|
st.markdown(""" |
|
<div class='custom-box info-box'> |
|
<h3>📊 تحليل المواقع</h3> |
|
<p>تحليل لمواقع المشاريع وتوزيعها الجغرافي.</p> |
|
<p>يمكنك عرض إحصائيات وتقارير متنوعة حول مواقع المشاريع.</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
if not st.session_state.project_locations: |
|
st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع من تبويب 'إدارة المواقع'.") |
|
return |
|
|
|
|
|
locations_df = pd.DataFrame(st.session_state.project_locations) |
|
|
|
|
|
st.markdown("### توزيع المشاريع حسب المدينة") |
|
|
|
city_counts = locations_df["city"].value_counts().reset_index() |
|
city_counts.columns = ["المدينة", "عدد المشاريع"] |
|
|
|
|
|
import plotly.express as px |
|
|
|
fig = px.bar( |
|
city_counts, |
|
x="المدينة", |
|
y="عدد المشاريع", |
|
title="توزيع المشاريع حسب المدينة", |
|
color="عدد المشاريع", |
|
color_continuous_scale="Viridis" |
|
) |
|
|
|
fig.update_layout( |
|
title_font_size=20, |
|
font_family="Arial", |
|
font_size=14, |
|
height=400 |
|
) |
|
|
|
st.plotly_chart(fig, use_container_width=True) |
|
|
|
|
|
st.markdown("### توزيع المشاريع حسب الحالة") |
|
|
|
status_counts = locations_df["status"].value_counts().reset_index() |
|
status_counts.columns = ["الحالة", "عدد المشاريع"] |
|
|
|
|
|
fig2 = px.pie( |
|
status_counts, |
|
values="عدد المشاريع", |
|
names="الحالة", |
|
title="توزيع المشاريع حسب الحالة", |
|
color_discrete_sequence=px.colors.qualitative.Set3 |
|
) |
|
|
|
fig2.update_layout( |
|
title_font_size=20, |
|
font_family="Arial", |
|
font_size=14, |
|
height=400 |
|
) |
|
|
|
st.plotly_chart(fig2, use_container_width=True) |
|
|
|
|
|
st.markdown("### تحليل المسافات بين المشاريع") |
|
|
|
|
|
if len(locations_df) > 1: |
|
|
|
reference_project = st.selectbox( |
|
"اختر مشروعًا كنقطة مرجعية", |
|
options=locations_df["project_id"].tolist(), |
|
format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x), |
|
key="reference_project" |
|
) |
|
|
|
|
|
ref_project_data = locations_df[locations_df["project_id"] == reference_project].iloc[0] |
|
|
|
|
|
distances = [] |
|
for _, project in locations_df.iterrows(): |
|
if project["project_id"] != reference_project: |
|
distance = self._calculate_distance( |
|
ref_project_data["latitude"], ref_project_data["longitude"], |
|
project["latitude"], project["longitude"] |
|
) |
|
|
|
distances.append({ |
|
"project_id": project["project_id"], |
|
"name": project["name"], |
|
"city": project["city"], |
|
"distance": distance |
|
}) |
|
|
|
|
|
distances_df = pd.DataFrame(distances) |
|
|
|
|
|
distances_df = distances_df.sort_values("distance") |
|
|
|
|
|
st.markdown(f"المسافات من مشروع: **{ref_project_data['name']}**") |
|
|
|
|
|
distances_df = distances_df.rename(columns={ |
|
"name": "اسم المشروع", |
|
"city": "المدينة", |
|
"distance": "المسافة (كم)" |
|
}) |
|
|
|
|
|
distances_df["المسافة (كم)"] = distances_df["المسافة (كم)"].round(2) |
|
|
|
|
|
st.dataframe(distances_df[["اسم المشروع", "المدينة", "المسافة (كم)"]], width=800) |
|
|
|
|
|
fig3 = px.bar( |
|
distances_df, |
|
x="اسم المشروع", |
|
y="المسافة (كم)", |
|
title=f"المسافات من مشروع {ref_project_data['name']}", |
|
color="المسافة (كم)", |
|
color_continuous_scale="Viridis" |
|
) |
|
|
|
fig3.update_layout( |
|
title_font_size=20, |
|
font_family="Arial", |
|
font_size=14, |
|
height=400 |
|
) |
|
|
|
st.plotly_chart(fig3, use_container_width=True) |
|
|
|
|
|
st.markdown("### المشاريع القريبة على الخريطة") |
|
|
|
|
|
m2 = folium.Map( |
|
location=[ref_project_data["latitude"], ref_project_data["longitude"]], |
|
zoom_start=8, |
|
tiles="OpenStreetMap", |
|
attr='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' |
|
) |
|
|
|
|
|
folium.Marker( |
|
location=[ref_project_data["latitude"], ref_project_data["longitude"]], |
|
popup=ref_project_data["name"], |
|
tooltip=ref_project_data["name"], |
|
icon=folium.Icon(color='red', icon='star') |
|
).add_to(m2) |
|
|
|
|
|
folium.Circle( |
|
location=[ref_project_data["latitude"], ref_project_data["longitude"]], |
|
radius=50000, |
|
color='red', |
|
fill=True, |
|
fill_opacity=0.1, |
|
popup="50 كم" |
|
).add_to(m2) |
|
|
|
folium.Circle( |
|
location=[ref_project_data["latitude"], ref_project_data["longitude"]], |
|
radius=100000, |
|
color='orange', |
|
fill=True, |
|
fill_opacity=0.1, |
|
popup="100 كم" |
|
).add_to(m2) |
|
|
|
folium.Circle( |
|
location=[ref_project_data["latitude"], ref_project_data["longitude"]], |
|
radius=200000, |
|
color='blue', |
|
fill=True, |
|
fill_opacity=0.1, |
|
popup="200 كم" |
|
).add_to(m2) |
|
|
|
|
|
for _, project in distances_df.iterrows(): |
|
project_data = locations_df[locations_df["project_id"] == project["project_id"]].iloc[0] |
|
|
|
folium.Marker( |
|
location=[project_data["latitude"], project_data["longitude"]], |
|
popup=f"{project_data['name']} - {project['المسافة (كم)']} كم", |
|
tooltip=project_data["name"], |
|
icon=folium.Icon(color='green', icon='info-sign') |
|
).add_to(m2) |
|
|
|
|
|
folium.PolyLine( |
|
locations=[ |
|
[ref_project_data["latitude"], ref_project_data["longitude"]], |
|
[project_data["latitude"], project_data["longitude"]] |
|
], |
|
color='gray', |
|
weight=2, |
|
opacity=0.5, |
|
popup=f"{project['المسافة (كم)']} كم" |
|
).add_to(m2) |
|
|
|
|
|
folium_static(m2, width=800, height=500) |
|
else: |
|
st.info("يجب وجود أكثر من مشروع واحد لحساب المسافات.") |
|
|
|
def _render_location_management(self): |
|
"""عرض إدارة المواقع""" |
|
st.markdown(""" |
|
<div class='custom-box info-box'> |
|
<h3>⚙️ إدارة المواقع</h3> |
|
<p>إضافة وتحرير وحذف مواقع المشاريع.</p> |
|
<p>يمكنك إضافة مواقع جديدة أو تحديث المواقع الموجودة.</p> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
management_tabs = st.tabs(["إضافة موقع جديد", "تحرير المواقع الموجودة", "استيراد وتصدير المواقع"]) |
|
|
|
|
|
with management_tabs[0]: |
|
self._render_add_location() |
|
|
|
|
|
with management_tabs[1]: |
|
self._render_edit_locations() |
|
|
|
|
|
with management_tabs[2]: |
|
self._render_import_export_locations() |
|
|
|
def _render_add_location(self): |
|
"""عرض نموذج إضافة موقع جديد""" |
|
st.markdown("### إضافة موقع مشروع جديد") |
|
|
|
|
|
project_name = st.text_input("اسم المشروع", key="new_project_name") |
|
project_desc = st.text_area("وصف المشروع", key="new_project_desc") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
project_city = st.text_input("المدينة", key="new_project_city") |
|
project_status = st.selectbox( |
|
"حالة المشروع", |
|
options=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"], |
|
key="new_project_status" |
|
) |
|
|
|
with col2: |
|
project_id = st.text_input("معرف المشروع (اختياري)", key="new_project_id", placeholder="سيتم إنشاؤه تلقائيًا إذا تُرك فارغًا") |
|
|
|
|
|
st.markdown("#### إحداثيات الموقع") |
|
location_method = st.radio( |
|
"طريقة تحديد الموقع", |
|
options=["إدخال يدوي", "اختيار من الخريطة"], |
|
key="new_location_method" |
|
) |
|
|
|
|
|
if location_method == "إدخال يدوي": |
|
loc_col1, loc_col2 = st.columns(2) |
|
|
|
with loc_col1: |
|
latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="new_latitude") |
|
|
|
with loc_col2: |
|
longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="new_longitude") |
|
|
|
|
|
mini_map = folium.Map(location=[latitude, longitude], zoom_start=10) |
|
folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map) |
|
folium_static(mini_map, width=700, height=300) |
|
else: |
|
st.markdown("#### اختر الموقع من الخريطة") |
|
st.info("انقر على الخريطة لتحديد الموقع.") |
|
|
|
|
|
m = folium.Map(location=[24.7136, 46.6753], zoom_start=6) |
|
|
|
|
|
m.add_child(folium.ClickForMarker(popup="الموقع المحدد")) |
|
|
|
|
|
map_data = folium_static(m, width=700, height=400) |
|
|
|
|
|
st.warning("ملاحظة: خاصية النقر على الخريطة غير مدعومة حاليًا في Streamlit. يرجى استخدام الإدخال اليدوي.") |
|
|
|
latitude = st.number_input("خط العرض", value=24.7136, step=0.0001, format="%.6f", key="map_latitude") |
|
longitude = st.number_input("خط الطول", value=46.6753, step=0.0001, format="%.6f", key="map_longitude") |
|
|
|
|
|
if styled_button("إضافة الموقع", key="add_location", type="primary", icon="➕"): |
|
if not project_name or not project_desc or not project_city: |
|
st.error("يرجى تعبئة جميع الحقول المطلوبة.") |
|
else: |
|
|
|
if not project_id: |
|
project_id = f"PRJ-{len(st.session_state.project_locations) + 1:04d}" |
|
|
|
|
|
new_location = { |
|
"name": project_name, |
|
"description": project_desc, |
|
"city": project_city, |
|
"status": project_status, |
|
"latitude": latitude, |
|
"longitude": longitude, |
|
"project_id": project_id, |
|
"created_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S"), |
|
"updated_at": pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S") |
|
} |
|
|
|
|
|
st.session_state.project_locations.append(new_location) |
|
|
|
|
|
self._save_locations_data() |
|
|
|
st.success(f"تمت إضافة موقع المشروع '{project_name}' بنجاح!") |
|
st.balloons() |
|
|
|
def _render_edit_locations(self): |
|
"""عرض واجهة تحرير المواقع الموجودة""" |
|
st.markdown("### تحرير أو حذف مواقع المشاريع") |
|
|
|
|
|
if not st.session_state.project_locations: |
|
st.warning("لا توجد مواقع مشاريع متاحة. يرجى إضافة مواقع أولاً.") |
|
return |
|
|
|
|
|
selected_project_id = st.selectbox( |
|
"اختر مشروعًا للتحرير", |
|
options=[p["project_id"] for p in st.session_state.project_locations], |
|
format_func=lambda x: next((p["name"] for p in st.session_state.project_locations if p["project_id"] == x), x), |
|
key="edit_project_select" |
|
) |
|
|
|
|
|
selected_project = next((p for p in st.session_state.project_locations if p["project_id"] == selected_project_id), None) |
|
|
|
if selected_project: |
|
|
|
st.markdown(f"### تحرير مشروع: {selected_project['name']}") |
|
|
|
|
|
project_name = st.text_input("اسم المشروع", value=selected_project["name"], key="edit_project_name") |
|
project_desc = st.text_area("وصف المشروع", value=selected_project["description"], key="edit_project_desc") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
project_city = st.text_input("المدينة", value=selected_project["city"], key="edit_project_city") |
|
project_status = st.selectbox( |
|
"حالة المشروع", |
|
options=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"], |
|
index=["جديد", "قيد التنفيذ", "متوقف", "مكتمل"].index(selected_project["status"]), |
|
key="edit_project_status" |
|
) |
|
|
|
with col2: |
|
st.text_input("معرف المشروع", value=selected_project["project_id"], disabled=True, key="edit_project_id") |
|
|
|
|
|
st.markdown("#### إحداثيات الموقع") |
|
|
|
|
|
loc_col1, loc_col2 = st.columns(2) |
|
|
|
with loc_col1: |
|
latitude = st.number_input( |
|
"خط العرض", |
|
value=selected_project["latitude"], |
|
step=0.0001, |
|
format="%.6f", |
|
key="edit_latitude" |
|
) |
|
|
|
with loc_col2: |
|
longitude = st.number_input( |
|
"خط الطول", |
|
value=selected_project["longitude"], |
|
step=0.0001, |
|
format="%.6f", |
|
key="edit_longitude" |
|
) |
|
|
|
|
|
mini_map = folium.Map(location=[latitude, longitude], zoom_start=10) |
|
folium.Marker(location=[latitude, longitude], tooltip="الموقع المحدد").add_to(mini_map) |
|
folium_static(mini_map, width=700, height=300) |
|
|
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
if styled_button("حفظ التغييرات", key="save_location_changes", type="primary", icon="💾"): |
|
if not project_name or not project_desc or not project_city: |
|
st.error("يرجى تعبئة جميع الحقول المطلوبة.") |
|
else: |
|
|
|
selected_project["name"] = project_name |
|
selected_project["description"] = project_desc |
|
selected_project["city"] = project_city |
|
selected_project["status"] = project_status |
|
selected_project["latitude"] = latitude |
|
selected_project["longitude"] = longitude |
|
selected_project["updated_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S") |
|
|
|
|
|
self._save_locations_data() |
|
|
|
st.success(f"تم تحديث بيانات المشروع '{project_name}' بنجاح!") |
|
st.experimental_rerun() |
|
|
|
with col2: |
|
if styled_button("حذف المشروع", key="delete_location", type="danger", icon="🗑️"): |
|
|
|
st.warning(f"هل أنت متأكد من حذف المشروع '{selected_project['name']}'؟") |
|
|
|
confirm_col1, confirm_col2 = st.columns(2) |
|
|
|
with confirm_col1: |
|
if styled_button("تأكيد الحذف", key="confirm_delete", type="danger", icon="✓"): |
|
|
|
st.session_state.project_locations.remove(selected_project) |
|
|
|
|
|
self._save_locations_data() |
|
|
|
st.success(f"تم حذف المشروع '{selected_project['name']}' بنجاح!") |
|
st.experimental_rerun() |
|
|
|
with confirm_col2: |
|
if styled_button("إلغاء", key="cancel_delete", type="secondary", icon="❌"): |
|
st.experimental_rerun() |
|
|
|
def _render_import_export_locations(self): |
|
"""عرض واجهة استيراد وتصدير المواقع""" |
|
st.markdown("### استيراد وتصدير مواقع المشاريع") |
|
|
|
col1, col2 = st.columns(2) |
|
|
|
with col1: |
|
st.markdown("#### تصدير المواقع") |
|
|
|
export_format = st.selectbox( |
|
"صيغة التصدير", |
|
options=["CSV", "JSON", "GeoJSON"], |
|
key="export_format" |
|
) |
|
|
|
if styled_button("تصدير المواقع", key="export_locations", type="primary", icon="📤"): |
|
self._export_locations(export_format) |
|
|
|
with col2: |
|
st.markdown("#### استيراد المواقع") |
|
|
|
import_format = st.selectbox( |
|
"صيغة الاستيراد", |
|
options=["CSV", "JSON", "GeoJSON"], |
|
key="import_format" |
|
) |
|
|
|
uploaded_file = st.file_uploader( |
|
"اختر ملف للاستيراد", |
|
type=["csv", "json", "geojson"], |
|
key="import_locations_file" |
|
) |
|
|
|
if uploaded_file is not None: |
|
if styled_button("استيراد المواقع", key="import_locations", type="success", icon="📥"): |
|
self._import_locations(uploaded_file, import_format) |
|
|
|
|
|
st.markdown("### إحصائيات البيانات") |
|
|
|
stats_col1, stats_col2, stats_col3 = st.columns(3) |
|
|
|
with stats_col1: |
|
st.metric("عدد المشاريع", len(st.session_state.project_locations)) |
|
|
|
with stats_col2: |
|
cities = set(p["city"] for p in st.session_state.project_locations) |
|
st.metric("عدد المدن", len(cities)) |
|
|
|
with stats_col3: |
|
statuses = {} |
|
for p in st.session_state.project_locations: |
|
statuses[p["status"]] = statuses.get(p["status"], 0) + 1 |
|
|
|
status_str = ", ".join([f"{k}: {v}" for k, v in statuses.items()]) |
|
st.metric("توزيع الحالات", status_str if statuses else "لا توجد بيانات") |
|
|
|
|
|
with st.expander("خيارات متقدمة"): |
|
if styled_button("حذف جميع المواقع", key="clear_locations", type="danger", icon="🗑️"): |
|
|
|
st.warning("هل أنت متأكد من حذف جميع مواقع المشاريع؟ لا يمكن التراجع عن هذا الإجراء.") |
|
|
|
confirm_col1, confirm_col2 = st.columns(2) |
|
|
|
with confirm_col1: |
|
if styled_button("تأكيد الحذف", key="confirm_clear", type="danger", icon="✓"): |
|
|
|
st.session_state.project_locations = [] |
|
|
|
|
|
self._save_locations_data() |
|
|
|
st.success("تم حذف جميع مواقع المشاريع بنجاح!") |
|
st.experimental_rerun() |
|
|
|
with confirm_col2: |
|
if styled_button("إلغاء", key="cancel_clear", type="secondary", icon="❌"): |
|
st.experimental_rerun() |
|
|
|
def _fetch_terrain_data(self, latitude, longitude, radius_km=5): |
|
"""جلب بيانات التضاريس من واجهة برمجة التطبيقات""" |
|
try: |
|
|
|
import plotly.express as px |
|
|
|
|
|
center_lat, center_lon = latitude, longitude |
|
|
|
|
|
radius_deg = radius_km / 111.0 |
|
|
|
|
|
min_lat = center_lat - radius_deg |
|
max_lat = center_lat + radius_deg |
|
min_lon = center_lon - radius_deg |
|
max_lon = center_lon + radius_deg |
|
|
|
|
|
resolution = 50 |
|
lats = np.linspace(min_lat, max_lat, resolution) |
|
lons = np.linspace(min_lon, max_lon, resolution) |
|
|
|
|
|
grid_lats, grid_lons = np.meshgrid(lats, lons) |
|
|
|
|
|
points = [] |
|
for i in range(grid_lats.shape[0]): |
|
for j in range(grid_lats.shape[1]): |
|
points.append((grid_lats[i, j], grid_lons[i, j])) |
|
|
|
|
|
batch_size = 100 |
|
batches = [points[i:i + batch_size] for i in range(0, len(points), batch_size)] |
|
|
|
|
|
elevation_data = np.zeros((len(lats), len(lons))) |
|
|
|
|
|
for batch_idx, batch in enumerate(batches): |
|
|
|
|
|
for point_idx, (lat, lon) in enumerate(batch): |
|
|
|
lat_idx = np.abs(lats - lat).argmin() |
|
lon_idx = np.abs(lons - lon).argmin() |
|
|
|
|
|
|
|
dist_from_center = np.sqrt( |
|
(lat - center_lat) ** 2 + (lon - center_lon) ** 2 |
|
) |
|
|
|
|
|
elevation = 500 + 200 * np.sin(dist_from_center * 100) + 100 * np.cos(lat * 30) + 150 * np.sin(lon * 40) |
|
|
|
|
|
elevation += np.random.normal(0, 30) |
|
|
|
|
|
elevation_data[lat_idx, lon_idx] = elevation |
|
|
|
|
|
elevation_stats = { |
|
"min": float(np.min(elevation_data)), |
|
"max": float(np.max(elevation_data)), |
|
"mean": float(np.mean(elevation_data)), |
|
"range": float(np.max(elevation_data) - np.min(elevation_data)) |
|
} |
|
|
|
|
|
center_lon_idx = np.abs(lons - center_lon).argmin() |
|
ns_profile = [] |
|
for i, lat in enumerate(lats): |
|
ns_profile.append({ |
|
"distance": (lat - min_lat) * 111.0, |
|
"elevation": float(elevation_data[i, center_lon_idx]) |
|
}) |
|
|
|
|
|
center_lat_idx = np.abs(lats - center_lat).argmin() |
|
ew_profile = [] |
|
for i, lon in enumerate(lons): |
|
ew_profile.append({ |
|
"distance": (lon - min_lon) * 111.0 * np.cos(np.radians(center_lat)), |
|
"elevation": float(elevation_data[center_lat_idx, i]) |
|
}) |
|
|
|
|
|
elevation_profile = ns_profile + ew_profile |
|
|
|
|
|
bounds = [min_lon, min_lat, max_lon, max_lat] |
|
|
|
|
|
terrain_array = elevation_data.astype(np.float32) |
|
|
|
|
|
terrain_data = [{ |
|
"bounds": bounds, |
|
"terrain": terrain_array.tolist(), |
|
"elevation_stats": elevation_stats, |
|
"elevation_profile": elevation_profile |
|
}] |
|
|
|
return terrain_data |
|
|
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء جلب بيانات التضاريس: {str(e)}") |
|
raise e |
|
|
|
def _calculate_distance(self, lat1, lon1, lat2, lon2): |
|
"""حساب المسافة بين نقطتين بالكيلومترات باستخدام صيغة هافرساين""" |
|
import math |
|
|
|
|
|
lat1 = math.radians(lat1) |
|
lon1 = math.radians(lon1) |
|
lat2 = math.radians(lat2) |
|
lon2 = math.radians(lon2) |
|
|
|
|
|
dlon = lon2 - lon1 |
|
dlat = lat2 - lat1 |
|
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2 |
|
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a)) |
|
distance = 6371 * c |
|
|
|
return distance |
|
|
|
def _get_color_map(self, scheme): |
|
"""الحصول على خريطة الألوان حسب النظام المختار""" |
|
if scheme == "terrain": |
|
return [ |
|
[0, (0, 50, 0)], |
|
[0.1, (0, 100, 0)], |
|
[0.25, (0, 150, 0)], |
|
[0.4, (200, 170, 0)], |
|
[0.6, (150, 100, 0)], |
|
[0.8, (100, 50, 0)], |
|
[1, (200, 200, 200)] |
|
] |
|
elif scheme == "elevation": |
|
return [ |
|
[0, (0, 0, 100)], |
|
[0.2, (0, 100, 150)], |
|
[0.4, (0, 150, 50)], |
|
[0.6, (150, 150, 0)], |
|
[0.8, (150, 50, 0)], |
|
[1, (100, 0, 0)] |
|
] |
|
else: |
|
return [ |
|
[0, (30, 100, 200)], |
|
[0.3, (60, 170, 250)], |
|
[0.5, (200, 220, 150)], |
|
[0.7, (180, 120, 60)], |
|
[0.9, (110, 60, 30)], |
|
[1, (80, 30, 10)] |
|
] |
|
|
|
def _export_locations(self, format): |
|
"""تصدير مواقع المشاريع إلى ملف""" |
|
try: |
|
if not st.session_state.project_locations: |
|
st.error("لا توجد مواقع مشاريع للتصدير.") |
|
return |
|
|
|
if format == "CSV": |
|
|
|
df = pd.DataFrame(st.session_state.project_locations) |
|
|
|
csv_data = df.to_csv(index=False) |
|
|
|
st.download_button( |
|
label="تنزيل ملف CSV", |
|
data=csv_data, |
|
file_name="project_locations.csv", |
|
mime="text/csv" |
|
) |
|
|
|
elif format == "JSON": |
|
|
|
json_data = json.dumps(st.session_state.project_locations, ensure_ascii=False, indent=2) |
|
|
|
st.download_button( |
|
label="تنزيل ملف JSON", |
|
data=json_data, |
|
file_name="project_locations.json", |
|
mime="application/json" |
|
) |
|
|
|
elif format == "GeoJSON": |
|
|
|
features = [] |
|
|
|
for location in st.session_state.project_locations: |
|
feature = { |
|
"type": "Feature", |
|
"geometry": { |
|
"type": "Point", |
|
"coordinates": [location["longitude"], location["latitude"]] |
|
}, |
|
"properties": { |
|
"name": location["name"], |
|
"description": location["description"], |
|
"city": location["city"], |
|
"status": location["status"], |
|
"project_id": location["project_id"], |
|
"created_at": location.get("created_at", ""), |
|
"updated_at": location.get("updated_at", "") |
|
} |
|
} |
|
|
|
features.append(feature) |
|
|
|
geojson = { |
|
"type": "FeatureCollection", |
|
"features": features |
|
} |
|
|
|
geojson_data = json.dumps(geojson, ensure_ascii=False, indent=2) |
|
|
|
st.download_button( |
|
label="تنزيل ملف GeoJSON", |
|
data=geojson_data, |
|
file_name="project_locations.geojson", |
|
mime="application/geo+json" |
|
) |
|
|
|
st.success(f"تم تصدير {len(st.session_state.project_locations)} موقع بنجاح!") |
|
|
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء تصدير المواقع: {str(e)}") |
|
|
|
def _import_locations(self, uploaded_file, format): |
|
"""استيراد مواقع المشاريع من ملف""" |
|
try: |
|
if format == "CSV": |
|
|
|
df = pd.read_csv(uploaded_file) |
|
|
|
|
|
required_columns = ["name", "latitude", "longitude"] |
|
missing_columns = [col for col in required_columns if col not in df.columns] |
|
|
|
if missing_columns: |
|
st.error(f"الملف لا يحتوي على الأعمدة التالية: {', '.join(missing_columns)}") |
|
return |
|
|
|
|
|
imported_locations = df.to_dict("records") |
|
|
|
elif format == "JSON": |
|
|
|
imported_locations = json.loads(uploaded_file.read()) |
|
|
|
elif format == "GeoJSON": |
|
|
|
geojson = json.loads(uploaded_file.read()) |
|
|
|
|
|
if "type" not in geojson or geojson["type"] != "FeatureCollection" or "features" not in geojson: |
|
st.error("تنسيق GeoJSON غير صحيح.") |
|
return |
|
|
|
|
|
imported_locations = [] |
|
|
|
for feature in geojson["features"]: |
|
if feature["type"] == "Feature" and feature["geometry"]["type"] == "Point": |
|
coords = feature["geometry"]["coordinates"] |
|
properties = feature["properties"] |
|
|
|
location = { |
|
"name": properties.get("name", ""), |
|
"description": properties.get("description", ""), |
|
"city": properties.get("city", ""), |
|
"status": properties.get("status", "جديد"), |
|
"longitude": coords[0], |
|
"latitude": coords[1], |
|
"project_id": properties.get("project_id", f"PRJ-{len(imported_locations)+1:04d}"), |
|
"created_at": properties.get("created_at", pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")), |
|
"updated_at": properties.get("updated_at", pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S")) |
|
} |
|
|
|
imported_locations.append(location) |
|
|
|
|
|
valid_locations = [] |
|
for location in imported_locations: |
|
|
|
if "name" not in location or "latitude" not in location or "longitude" not in location: |
|
continue |
|
|
|
|
|
if "description" not in location: |
|
location["description"] = "" |
|
|
|
if "city" not in location: |
|
location["city"] = "" |
|
|
|
if "status" not in location: |
|
location["status"] = "جديد" |
|
|
|
if "project_id" not in location: |
|
location["project_id"] = f"PRJ-{len(valid_locations)+1:04d}" |
|
|
|
if "created_at" not in location: |
|
location["created_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S") |
|
|
|
if "updated_at" not in location: |
|
location["updated_at"] = pd.Timestamp.now().strftime("%Y-%m-%d %H:%M:%S") |
|
|
|
valid_locations.append(location) |
|
|
|
if not valid_locations: |
|
st.error("لم يتم العثور على مواقع صالحة في الملف.") |
|
return |
|
|
|
|
|
import_mode = st.radio( |
|
"كيفية الاستيراد", |
|
options=["إضافة إلى المواقع الموجودة", "استبدال المواقع الموجودة"], |
|
key="import_mode" |
|
) |
|
|
|
if styled_button("تأكيد الاستيراد", key="confirm_import", type="success", icon="✓"): |
|
if import_mode == "إضافة إلى المواقع الموجودة": |
|
|
|
st.session_state.project_locations.extend(valid_locations) |
|
else: |
|
|
|
st.session_state.project_locations = valid_locations |
|
|
|
|
|
self._save_locations_data() |
|
|
|
st.success(f"تم استيراد {len(valid_locations)} موقع بنجاح!") |
|
st.experimental_rerun() |
|
|
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء استيراد المواقع: {str(e)}") |
|
|
|
def _save_locations_data(self): |
|
"""حفظ بيانات المواقع""" |
|
try: |
|
|
|
os.makedirs(self.data_dir, exist_ok=True) |
|
|
|
|
|
locations_file = os.path.join(self.data_dir, "project_locations.json") |
|
|
|
with open(locations_file, 'w', encoding='utf-8') as f: |
|
json.dump(st.session_state.project_locations, f, ensure_ascii=False, indent=2) |
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء حفظ بيانات المواقع: {str(e)}") |
|
|
|
def _load_locations_data(self): |
|
"""تحميل بيانات المواقع""" |
|
try: |
|
|
|
locations_file = os.path.join(self.data_dir, "project_locations.json") |
|
|
|
if os.path.exists(locations_file): |
|
with open(locations_file, 'r', encoding='utf-8') as f: |
|
locations = json.load(f) |
|
|
|
|
|
st.session_state.project_locations = locations |
|
except Exception as e: |
|
st.error(f"حدث خطأ أثناء تحميل بيانات المواقع: {str(e)}") |
|
|
|
def _initialize_sample_projects(self): |
|
"""تهيئة بيانات اختبارية للمشاريع""" |
|
|
|
locations_file = os.path.join(self.data_dir, "project_locations.json") |
|
|
|
if os.path.exists(locations_file): |
|
|
|
self._load_locations_data() |
|
return |
|
|
|
|
|
sample_projects = [ |
|
{ |
|
"name": "تطوير شبكة الطرق في منطقة الرياض", |
|
"description": "مشروع تطوير وتوسعة شبكة الطرق الرئيسية في منطقة الرياض", |
|
"city": "الرياض", |
|
"status": "قيد التنفيذ", |
|
"latitude": 24.7136, |
|
"longitude": 46.6753, |
|
"project_id": "PRJ-0001", |
|
"created_at": "2025-01-15 10:30:00", |
|
"updated_at": "2025-01-15 10:30:00" |
|
}, |
|
{ |
|
"name": "إنشاء سد وادي حنيفة", |
|
"description": "مشروع إنشاء سد لحجز مياه الأمطار في وادي حنيفة", |
|
"city": "الرياض", |
|
"status": "جديد", |
|
"latitude": 24.6748, |
|
"longitude": 46.5831, |
|
"project_id": "PRJ-0002", |
|
"created_at": "2025-02-01 14:45:00", |
|
"updated_at": "2025-02-01 14:45:00" |
|
}, |
|
{ |
|
"name": "تطوير ميناء جدة الإسلامي", |
|
"description": "مشروع تطوير وتوسعة ميناء جدة الإسلامي لزيادة الطاقة الاستيعابية", |
|
"city": "جدة", |
|
"status": "قيد التنفيذ", |
|
"latitude": 21.4858, |
|
"longitude": 39.1925, |
|
"project_id": "PRJ-0003", |
|
"created_at": "2024-11-20 09:15:00", |
|
"updated_at": "2024-11-20 09:15:00" |
|
}, |
|
{ |
|
"name": "إنشاء مطار الدمام الجديد", |
|
"description": "مشروع إنشاء مطار جديد في مدينة الدمام لتلبية الطلب المتزايد", |
|
"city": "الدمام", |
|
"status": "متوقف", |
|
"latitude": 26.4207, |
|
"longitude": 50.0888, |
|
"project_id": "PRJ-0004", |
|
"created_at": "2024-10-05 11:30:00", |
|
"updated_at": "2024-10-05 11:30:00" |
|
}, |
|
{ |
|
"name": "توسعة جامعة الملك فهد للبترول والمعادن", |
|
"description": "مشروع توسعة مباني ومرافق جامعة الملك فهد للبترول والمعادن", |
|
"city": "الظهران", |
|
"status": "قيد التنفيذ", |
|
"latitude": 26.3927, |
|
"longitude": 50.1150, |
|
"project_id": "PRJ-0005", |
|
"created_at": "2025-01-10 08:00:00", |
|
"updated_at": "2025-01-10 08:00:00" |
|
}, |
|
{ |
|
"name": "إنشاء محطة تحلية مياه القنفذة", |
|
"description": "مشروع إنشاء محطة تحلية مياه جديدة في محافظة القنفذة", |
|
"city": "القنفذة", |
|
"status": "جديد", |
|
"latitude": 19.1299, |
|
"longitude": 41.0825, |
|
"project_id": "PRJ-0006", |
|
"created_at": "2025-02-20 15:20:00", |
|
"updated_at": "2025-02-20 15:20:00" |
|
}, |
|
{ |
|
"name": "تطوير مجمع حكومي في حائل", |
|
"description": "مشروع إنشاء وتطوير مجمع للدوائر الحكومية في مدينة حائل", |
|
"city": "حائل", |
|
"status": "مكتمل", |
|
"latitude": 27.5114, |
|
"longitude": 41.7208, |
|
"project_id": "PRJ-0007", |
|
"created_at": "2024-06-15 10:00:00", |
|
"updated_at": "2024-12-10 14:30:00" |
|
}, |
|
{ |
|
"name": "إنشاء مستشفى الإحساء العام", |
|
"description": "مشروع إنشاء مستشفى عام جديد في محافظة الإحساء بسعة 500 سرير", |
|
"city": "الإحساء", |
|
"status": "قيد التنفيذ", |
|
"latitude": 25.3753, |
|
"longitude": 49.5873, |
|
"project_id": "PRJ-0008", |
|
"created_at": "2024-09-01 09:45:00", |
|
"updated_at": "2024-09-01 09:45:00" |
|
}, |
|
{ |
|
"name": "تطوير شبكة الصرف الصحي في أبها", |
|
"description": "مشروع تطوير وتوسعة شبكة الصرف الصحي في مدينة أبها", |
|
"city": "أبها", |
|
"status": "جديد", |
|
"latitude": 18.2164, |
|
"longitude": 42.5053, |
|
"project_id": "PRJ-0009", |
|
"created_at": "2025-02-25 11:15:00", |
|
"updated_at": "2025-02-25 11:15:00" |
|
}, |
|
{ |
|
"name": "إنشاء مدينة صناعية في سكاكا", |
|
"description": "مشروع إنشاء مدينة صناعية جديدة في منطقة سكاكا", |
|
"city": "سكاكا", |
|
"status": "متوقف", |
|
"latitude": 29.9720, |
|
"longitude": 40.2006, |
|
"project_id": "PRJ-0010", |
|
"created_at": "2024-07-20 13:30:00", |
|
"updated_at": "2024-07-20 13:30:00" |
|
} |
|
] |
|
|
|
|
|
st.session_state.project_locations = sample_projects |
|
|
|
|
|
self._save_locations_data() |
|
|
|
|
|
|
|
class folium_static: |
|
"""فئة لعرض خرائط Folium في Streamlit""" |
|
|
|
def __init__(self, fig, width=700, height=500): |
|
"""عرض خريطة Folium في Streamlit""" |
|
import streamlit.components.v1 as components |
|
|
|
|
|
fig_html = fig._repr_html_() |
|
|
|
|
|
components.html(fig_html, width=width, height=height) |
|
|
|
|
|
|
|
def main(): |
|
"""تشغيل وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد بشكل مستقل""" |
|
|
|
st.set_page_config( |
|
page_title="الخريطة التفاعلية | WAHBi AI", |
|
page_icon="🗺️", |
|
layout="wide", |
|
initial_sidebar_state="expanded", |
|
menu_items={ |
|
'Get Help': 'mailto:[email protected]', |
|
'Report a bug': 'mailto:[email protected]', |
|
'About': 'وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد - جزء من نظام WAHBi AI لتحليل المناقصات' |
|
} |
|
) |
|
|
|
|
|
interactive_map = InteractiveMap() |
|
|
|
|
|
interactive_map.render() |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |