| | """ |
| | Map Viewer Component |
| | ==================== |
| | Handles map display and vehicle path drawing using Folium. |
| | """ |
| |
|
| | import streamlit as st |
| | import folium |
| | from streamlit_folium import st_folium, folium_static |
| | from folium.plugins import Draw |
| | import json |
| | from typing import List |
| |
|
| | from config import CASE_STUDY_LOCATION, MAP_CONFIG, COLORS |
| |
|
| |
|
| | def create_base_map(location: dict = None) -> folium.Map: |
| | """ |
| | Create a base Folium map centered on the accident location. |
| | |
| | Args: |
| | location: Dictionary with latitude, longitude, and name |
| | |
| | Returns: |
| | Folium Map object |
| | """ |
| | if location is None: |
| | location = CASE_STUDY_LOCATION |
| | |
| | |
| | m = folium.Map( |
| | location=[location['latitude'], location['longitude']], |
| | zoom_start=MAP_CONFIG['default_zoom'], |
| | tiles='OpenStreetMap' |
| | ) |
| | |
| | |
| | folium.Marker( |
| | location=[location['latitude'], location['longitude']], |
| | popup=location.get('name', 'Accident Location'), |
| | icon=folium.Icon(color='red', icon='info-sign'), |
| | tooltip="Accident Location" |
| | ).add_to(m) |
| | |
| | |
| | folium.Circle( |
| | location=[location['latitude'], location['longitude']], |
| | radius=location.get('radius_meters', 200), |
| | color='blue', |
| | fill=True, |
| | fill_opacity=0.1, |
| | popup="Analysis Area" |
| | ).add_to(m) |
| | |
| | return m |
| |
|
| |
|
| | def add_draw_control(m: folium.Map) -> folium.Map: |
| | """ |
| | Add drawing controls to the map for path definition. |
| | |
| | Args: |
| | m: Folium Map object |
| | |
| | Returns: |
| | Folium Map with draw controls |
| | """ |
| | draw = Draw( |
| | draw_options={ |
| | 'polyline': { |
| | 'allowIntersection': True, |
| | 'shapeOptions': { |
| | 'color': '#FF4B4B', |
| | 'weight': 4 |
| | } |
| | }, |
| | 'polygon': False, |
| | 'circle': False, |
| | 'rectangle': False, |
| | 'circlemarker': False, |
| | 'marker': True |
| | }, |
| | edit_options={'edit': True, 'remove': True} |
| | ) |
| | draw.add_to(m) |
| | |
| | return m |
| |
|
| |
|
| | def add_vehicle_path(m: folium.Map, path: list, vehicle_id: int) -> folium.Map: |
| | """ |
| | Add a vehicle path to the map. |
| | |
| | Args: |
| | m: Folium Map object |
| | path: List of [lat, lng] coordinates |
| | vehicle_id: 1 or 2 to determine color |
| | |
| | Returns: |
| | Folium Map with vehicle path |
| | """ |
| | if not path or len(path) < 2: |
| | return m |
| | |
| | color = COLORS['vehicle_1'] if vehicle_id == 1 else COLORS['vehicle_2'] |
| | |
| | |
| | folium.PolyLine( |
| | locations=path, |
| | color=color, |
| | weight=4, |
| | opacity=0.8, |
| | popup=f"Vehicle {vehicle_id} Path", |
| | tooltip=f"Vehicle {vehicle_id}" |
| | ).add_to(m) |
| | |
| | |
| | folium.Marker( |
| | location=path[0], |
| | popup=f"Vehicle {vehicle_id} Start", |
| | icon=folium.Icon( |
| | color='green' if vehicle_id == 1 else 'blue', |
| | icon='play' |
| | ) |
| | ).add_to(m) |
| | |
| | |
| | folium.Marker( |
| | location=path[-1], |
| | popup=f"Vehicle {vehicle_id} End", |
| | icon=folium.Icon( |
| | color='red' if vehicle_id == 1 else 'darkblue', |
| | icon='stop' |
| | ) |
| | ).add_to(m) |
| | |
| | |
| | for i in range(len(path) - 1): |
| | mid_lat = (path[i][0] + path[i+1][0]) / 2 |
| | mid_lng = (path[i][1] + path[i+1][1]) / 2 |
| | |
| | folium.RegularPolygonMarker( |
| | location=[mid_lat, mid_lng], |
| | number_of_sides=3, |
| | radius=8, |
| | color=color, |
| | fill=True, |
| | fill_color=color, |
| | fill_opacity=0.7 |
| | ).add_to(m) |
| | |
| | return m |
| |
|
| |
|
| | def add_collision_point(m: folium.Map, collision_point: list) -> folium.Map: |
| | """ |
| | Add a collision point marker to the map. |
| | |
| | Args: |
| | m: Folium Map object |
| | collision_point: [lat, lng] of collision |
| | |
| | Returns: |
| | Folium Map with collision marker |
| | """ |
| | if not collision_point: |
| | return m |
| | |
| | folium.Marker( |
| | location=collision_point, |
| | popup="Estimated Collision Point", |
| | icon=folium.Icon(color='orange', icon='warning-sign'), |
| | tooltip="💥 Collision Point" |
| | ).add_to(m) |
| | |
| | |
| | folium.Circle( |
| | location=collision_point, |
| | radius=10, |
| | color=COLORS['collision_point'], |
| | fill=True, |
| | fill_opacity=0.5, |
| | popup="Impact Zone" |
| | ).add_to(m) |
| | |
| | return m |
| |
|
| |
|
| | def render_map_section(vehicle_id: int = None): |
| | """ |
| | Render the map section in Streamlit. |
| | |
| | Args: |
| | vehicle_id: If provided, enables path drawing for that vehicle |
| | """ |
| | location = st.session_state.accident_info['location'] |
| | |
| | |
| | m = create_base_map(location) |
| | |
| | |
| | if st.session_state.vehicle_1.get('path'): |
| | m = add_vehicle_path(m, st.session_state.vehicle_1['path'], 1) |
| | |
| | if st.session_state.vehicle_2.get('path'): |
| | m = add_vehicle_path(m, st.session_state.vehicle_2['path'], 2) |
| | |
| | |
| | if vehicle_id: |
| | m = add_draw_control(m) |
| | |
| | st.info(f"🖊️ Draw the path for Vehicle {vehicle_id} on the map. Click points to create a path, then click 'Finish' to complete.") |
| | |
| | |
| | map_data = st_folium( |
| | m, |
| | width=700, |
| | height=500, |
| | returned_objects=["last_active_drawing", "all_drawings"] |
| | ) |
| | |
| | |
| | if vehicle_id and map_data and map_data.get('last_active_drawing'): |
| | drawing = map_data['last_active_drawing'] |
| | |
| | if drawing.get('geometry', {}).get('type') == 'LineString': |
| | coords = drawing['geometry']['coordinates'] |
| | |
| | path = [[c[1], c[0]] for c in coords] |
| | |
| | if vehicle_id == 1: |
| | st.session_state.vehicle_1['path'] = path |
| | else: |
| | st.session_state.vehicle_2['path'] = path |
| | |
| | st.success(f"✅ Path saved for Vehicle {vehicle_id}!") |
| | |
| | |
| | if vehicle_id: |
| | vehicle_key = f'vehicle_{vehicle_id}' |
| | current_path = st.session_state[vehicle_key].get('path', []) |
| | |
| | if current_path: |
| | st.success(f"**✅ Path defined:** {len(current_path)} points") |
| | else: |
| | st.warning("⚠️ No path drawn yet. Use the drawing tools on the map OR select a preset path below.") |
| | |
| | |
| | st.markdown("---") |
| | st.markdown("**🛣️ Quick Path Selection (Roundabout)**") |
| | |
| | preset_col1, preset_col2 = st.columns(2) |
| | |
| | with preset_col1: |
| | entry_direction = st.selectbox( |
| | f"Entry Direction (V{vehicle_id})", |
| | options=['north', 'south', 'east', 'west'], |
| | key=f"entry_dir_{vehicle_id}" |
| | ) |
| | |
| | with preset_col2: |
| | exit_direction = st.selectbox( |
| | f"Exit Direction (V{vehicle_id})", |
| | options=['north', 'south', 'east', 'west'], |
| | index=2 if entry_direction == 'north' else 0, |
| | key=f"exit_dir_{vehicle_id}" |
| | ) |
| | |
| | if st.button(f"🔄 Generate Path for Vehicle {vehicle_id}", key=f"gen_path_{vehicle_id}"): |
| | generated_path = generate_roundabout_path( |
| | location['latitude'], |
| | location['longitude'], |
| | entry_direction, |
| | exit_direction |
| | ) |
| | |
| | if vehicle_id == 1: |
| | st.session_state.vehicle_1['path'] = generated_path |
| | else: |
| | st.session_state.vehicle_2['path'] = generated_path |
| | |
| | st.success(f"✅ Path generated: {entry_direction.title()} → {exit_direction.title()}") |
| | st.rerun() |
| | |
| | |
| | with st.expander("📝 Or enter path manually"): |
| | st.write("Enter coordinates as: lat1,lng1;lat2,lng2;...") |
| | manual_path = st.text_input( |
| | "Path coordinates", |
| | key=f"manual_path_{vehicle_id}", |
| | placeholder="26.2397,50.5369;26.2400,50.5372" |
| | ) |
| | |
| | if st.button(f"Set Manual Path", key=f"set_path_{vehicle_id}"): |
| | if manual_path: |
| | try: |
| | path = [] |
| | for point in manual_path.split(';'): |
| | lat, lng = point.strip().split(',') |
| | path.append([float(lat), float(lng)]) |
| | |
| | if vehicle_id == 1: |
| | st.session_state.vehicle_1['path'] = path |
| | else: |
| | st.session_state.vehicle_2['path'] = path |
| | |
| | st.success(f"Path set with {len(path)} points!") |
| | st.rerun() |
| | except Exception as e: |
| | st.error(f"Invalid format: {e}") |
| |
|
| |
|
| | def generate_roundabout_path( |
| | center_lat: float, |
| | center_lng: float, |
| | entry_direction: str, |
| | exit_direction: str |
| | ) -> List[List[float]]: |
| | """ |
| | Generate a realistic path through a roundabout. |
| | |
| | Args: |
| | center_lat: Center latitude of roundabout |
| | center_lng: Center longitude of roundabout |
| | entry_direction: Direction vehicle enters from |
| | exit_direction: Direction vehicle exits to |
| | |
| | Returns: |
| | List of [lat, lng] coordinates forming the path |
| | """ |
| | import math |
| | |
| | |
| | approach_distance = 0.0015 |
| | roundabout_radius = 0.0004 |
| | |
| | |
| | dir_to_angle = { |
| | 'north': 90, |
| | 'east': 0, |
| | 'south': 270, |
| | 'west': 180 |
| | } |
| | |
| | entry_angle = math.radians(dir_to_angle[entry_direction]) |
| | exit_angle = math.radians(dir_to_angle[exit_direction]) |
| | |
| | path = [] |
| | |
| | |
| | entry_lat = center_lat + approach_distance * math.sin(entry_angle) |
| | entry_lng = center_lng + approach_distance * math.cos(entry_angle) |
| | path.append([entry_lat, entry_lng]) |
| | |
| | |
| | edge_lat = center_lat + roundabout_radius * 1.5 * math.sin(entry_angle) |
| | edge_lng = center_lng + roundabout_radius * 1.5 * math.cos(entry_angle) |
| | path.append([edge_lat, edge_lng]) |
| | |
| | |
| | entry_deg = dir_to_angle[entry_direction] |
| | exit_deg = dir_to_angle[exit_direction] |
| | |
| | |
| | if exit_deg <= entry_deg: |
| | exit_deg += 360 |
| | |
| | |
| | num_arc_points = max(2, (exit_deg - entry_deg) // 45) |
| | for i in range(1, num_arc_points + 1): |
| | angle = entry_deg + (exit_deg - entry_deg) * i / (num_arc_points + 1) |
| | angle_rad = math.radians(angle) |
| | arc_lat = center_lat + roundabout_radius * math.sin(angle_rad) |
| | arc_lng = center_lng + roundabout_radius * math.cos(angle_rad) |
| | path.append([arc_lat, arc_lng]) |
| | |
| | |
| | exit_edge_lat = center_lat + roundabout_radius * 1.5 * math.sin(exit_angle) |
| | exit_edge_lng = center_lng + roundabout_radius * 1.5 * math.cos(exit_angle) |
| | path.append([exit_edge_lat, exit_edge_lng]) |
| | |
| | |
| | exit_lat = center_lat + approach_distance * math.sin(exit_angle) |
| | exit_lng = center_lng + approach_distance * math.cos(exit_angle) |
| | path.append([exit_lat, exit_lng]) |
| | |
| | return path |
| |
|
| |
|
| | def render_results_map(scenarios: list, selected_scenario: int = 0): |
| | """ |
| | Render a map showing analysis results for a specific scenario. |
| | |
| | Args: |
| | scenarios: List of generated scenarios |
| | selected_scenario: Index of selected scenario |
| | """ |
| | if not scenarios: |
| | st.warning("No scenarios to display") |
| | return |
| | |
| | scenario = scenarios[selected_scenario] |
| | location = st.session_state.accident_info['location'] |
| | |
| | |
| | m = create_base_map(location) |
| | |
| | |
| | if scenario.get('vehicle_1_path'): |
| | m = add_vehicle_path(m, scenario['vehicle_1_path'], 1) |
| | |
| | if scenario.get('vehicle_2_path'): |
| | m = add_vehicle_path(m, scenario['vehicle_2_path'], 2) |
| | |
| | |
| | if scenario.get('collision_point'): |
| | m = add_collision_point(m, scenario['collision_point']) |
| | |
| | |
| | info_html = f""" |
| | <div style="width: 200px;"> |
| | <h4>Scenario {selected_scenario + 1}</h4> |
| | <p><b>Probability:</b> {scenario.get('probability', 0)*100:.1f}%</p> |
| | <p><b>Type:</b> {scenario.get('accident_type', 'Unknown')}</p> |
| | </div> |
| | """ |
| | |
| | folium.Marker( |
| | location=[location['latitude'], location['longitude']], |
| | popup=folium.Popup(info_html, max_width=250), |
| | icon=folium.Icon(color='purple', icon='info-sign') |
| | ).add_to(m) |
| | |
| | |
| | folium_static(m, width=700, height=500) |
| |
|