| | |
| | |
| |
|
| | """ |
| | وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد |
| | تتيح هذه الوحدة عرض مواقع المشاريع على خريطة تفاعلية مع إمكانية رؤية التضاريس بشكل ثلاثي الأبعاد |
| | """ |
| |
|
| | 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:support@wahbi-ai.com', |
| | 'Report a bug': 'mailto:support@wahbi-ai.com', |
| | 'About': 'وحدة الخريطة التفاعلية مع عرض التضاريس ثلاثي الأبعاد - جزء من نظام WAHBi AI لتحليل المناقصات' |
| | } |
| | ) |
| | |
| | |
| | interactive_map = InteractiveMap() |
| | |
| | |
| | interactive_map.render() |
| |
|
| | |
| | if __name__ == "__main__": |
| | main() |