Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, Request, Query, HTTPException, Response | |
| from fastapi.responses import JSONResponse, HTMLResponse, FileResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| import pandas as pd | |
| import numpy as np | |
| from geopy.distance import geodesic | |
| import folium | |
| from folium import plugins | |
| import osmnx as ox | |
| import networkx as nx | |
| from datetime import datetime | |
| import json | |
| import matplotlib.pyplot as plt | |
| import plotly.express as px | |
| import os | |
| import time | |
| from functools import lru_cache | |
| from rtree import index | |
| import gc | |
| import shutil | |
| from typing import Optional, List, Dict, Any, Union | |
| from pydantic import BaseModel, Field | |
| app = FastAPI(title="falcao-maps API", description="Store locator and route planning API") | |
| # Add CORS middleware | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # Create temp directory for files | |
| os.makedirs('temp', exist_ok=True) | |
| # Custom JSON encoder for NumPy types | |
| class NumpyEncoder(json.JSONEncoder): | |
| def default(self, obj): | |
| if isinstance(obj, np.integer): | |
| return int(obj) | |
| elif isinstance(obj, np.floating): | |
| return float(obj) | |
| elif isinstance(obj, np.ndarray): | |
| return obj.tolist() | |
| return super(NumpyEncoder, self).default(obj) | |
| # Load and prepare the store data | |
| stores_df = pd.read_csv('dataset of 50 stores.csv') | |
| # Define Pydantic models for API responses | |
| class Location(BaseModel): | |
| lat: float | |
| lon: float | |
| class Store(BaseModel): | |
| store_name: str | |
| address: str | |
| contact: str | |
| distance: float | |
| estimated_delivery_time: int | |
| product_categories: str | |
| location: Location | |
| class StoresResponse(BaseModel): | |
| status: str | |
| stores: List[Store] | |
| class ErrorResponse(BaseModel): | |
| status: str | |
| message: str | |
| class StoreLocator: | |
| def __init__(self, stores_dataframe): | |
| self.stores_df = stores_dataframe | |
| self.network_graph = None | |
| self.graph_cache = {} # Cache for network graphs | |
| self.spatial_index = self._build_spatial_index() | |
| def initialize_graph(self, center_point, dist=30000): # Reduced distance for memory optimization | |
| """Initialize road network graph with caching""" | |
| cache_key = f"{center_point[0]}_{center_point[1]}" | |
| if cache_key in self.graph_cache: | |
| self.network_graph = self.graph_cache[cache_key] | |
| return True | |
| try: | |
| # Use simplify=True and increased tolerance for lower memory usage | |
| self.network_graph = ox.graph_from_point( | |
| center_point, | |
| dist=dist, | |
| network_type="drive", | |
| simplify=True, | |
| retain_all=False | |
| ) | |
| self.network_graph = ox.add_edge_speeds(self.network_graph) | |
| self.network_graph = ox.add_edge_travel_times(self.network_graph) | |
| # Store in cache | |
| self.graph_cache[cache_key] = self.network_graph | |
| # Force garbage collection | |
| gc.collect() | |
| return True | |
| except Exception as e: | |
| print(f"Error initializing graph: {str(e)}") | |
| return False | |
| def _build_spatial_index(self): | |
| idx = index.Index() | |
| for i, row in self.stores_df.iterrows(): | |
| idx.insert(i, (row['Latitude'], row['Longitude'], | |
| row['Latitude'], row['Longitude'])) | |
| return idx | |
| def calculate_distance(self, lat1, lon1, lat2, lon2): | |
| """Calculate direct distance between two points""" | |
| return geodesic((lat1, lon1), (lat2, lon2)).kilometers | |
| def estimate_delivery_time(self, distance, current_time=None): | |
| """Estimate delivery time based on distance and current time""" | |
| if current_time is None: | |
| current_time = datetime.now() | |
| # Base time: 5 mins base + 2 mins per km | |
| base_minutes = 5 + (distance * 2) | |
| # Apply traffic multiplier based on time of day | |
| hour = current_time.hour | |
| if hour in [8, 9, 10, 17, 18, 19]: # Peak hours | |
| multiplier = 1.5 | |
| elif hour in [23, 0, 1, 2, 3, 4]: # Off-peak hours | |
| multiplier = 0.8 | |
| else: # Normal hours | |
| multiplier = 1.0 | |
| return round(base_minutes * multiplier) | |
| def find_nearby_stores(self, lat, lon, radius=5): | |
| """Find stores within radius using spatial index""" | |
| nearby_stores = [] | |
| bbox = (lat - radius/111.0, lon - radius/111.0, | |
| lat + radius/111.0, lon + radius/111.0) | |
| for store_id in self.spatial_index.intersection(bbox): | |
| store = self.stores_df.iloc[store_id] | |
| distance = self.calculate_distance(lat, lon, | |
| store['Latitude'], | |
| store['Longitude']) | |
| if distance <= radius: | |
| delivery_time = self.estimate_delivery_time(distance) | |
| nearby_stores.append({ | |
| 'store_name': store['Store Name'], | |
| 'address': store['Address'], | |
| 'contact': str(store['Contact Number']), # Convert to string to avoid int64 issues | |
| 'distance': round(distance, 2), | |
| 'estimated_delivery_time': int(delivery_time), # Ensure integer type | |
| 'product_categories': store['Product Categories'], | |
| 'location': { | |
| 'lat': float(store['Latitude']), # Ensure float type | |
| 'lon': float(store['Longitude']) # Ensure float type | |
| } | |
| }) | |
| return sorted(nearby_stores, key=lambda x: x['distance']) | |
| def create_store_map(self, center_lat, center_lon, radius=5): | |
| """Create an interactive map with store locations - optimized for memory""" | |
| # Create base map | |
| m = folium.Map( | |
| location=[center_lat, center_lon], | |
| zoom_start=13, | |
| tiles="cartodbpositron" | |
| ) | |
| # Create marker cluster for better performance with many markers | |
| marker_cluster = plugins.MarkerCluster().add_to(m) | |
| # Add stores to map | |
| nearby_stores = self.find_nearby_stores(center_lat, center_lon, radius) | |
| # Limit the number of stores to reduce memory usage | |
| max_stores = min(len(nearby_stores), 50) # Cap at 50 stores | |
| for store in nearby_stores[:max_stores]: | |
| # Prepare popup content | |
| popup_content = f""" | |
| <div style='width: 200px'> | |
| <b>{store['store_name']}</b><br> | |
| Address: {store['address']}<br> | |
| Distance: {store['distance']} km<br> | |
| Est. Delivery: {store['estimated_delivery_time']} mins<br> | |
| Categories: {store['product_categories']} | |
| </div> | |
| """ | |
| # Add store marker | |
| folium.Marker( | |
| location=[store['location']['lat'], store['location']['lon']], | |
| popup=folium.Popup(popup_content, max_width=300), | |
| icon=folium.Icon(color='red', icon='info-sign') | |
| ).add_to(marker_cluster) | |
| # Add line to show distance from center (only for closer stores) | |
| if store['distance'] <= nearby_stores[min(9, len(nearby_stores)-1)]['distance']: | |
| folium.PolyLine( | |
| locations=[[center_lat, center_lon], | |
| [store['location']['lat'], store['location']['lon']]], | |
| weight=2, | |
| color='blue', | |
| opacity=0.3 | |
| ).add_to(m) | |
| # Add current location marker | |
| folium.Marker( | |
| location=[center_lat, center_lon], | |
| popup='Your Location', | |
| icon=folium.Icon(color='green', icon='home') | |
| ).add_to(m) | |
| # Add layer control only | |
| folium.LayerControl().add_to(m) | |
| return m | |
| # Initialize store locator | |
| store_locator = StoreLocator(stores_df) | |
| # Helper functions for cleaning temporary files | |
| def cleanup_temp_files(): | |
| temp_dir = 'temp' | |
| if os.path.exists(temp_dir): | |
| for file in os.listdir(temp_dir): | |
| file_path = os.path.join(temp_dir, file) | |
| try: | |
| if os.path.isfile(file_path) and file.endswith('.html'): | |
| # Delete files older than 1 hour | |
| if os.path.getmtime(file_path) < time.time() - 3600: | |
| os.remove(file_path) | |
| except Exception as e: | |
| print(f"Error cleaning up temp files: {e}") | |
| # Register cleanup on startup and shutdown | |
| async def startup_event(): | |
| cleanup_temp_files() | |
| async def shutdown_event(): | |
| # Clean up all temporary files on shutdown | |
| try: | |
| shutil.rmtree('temp') | |
| except Exception as e: | |
| print(f"Error cleaning up temp directory: {e}") | |
| # Routes | |
| async def home(): | |
| """API Documentation Homepage""" | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>falcao-maps API Documentation</title> | |
| <style> | |
| body {{ | |
| font-family: Arial, sans-serif; | |
| margin: 20px; | |
| }} | |
| h1, h2 {{ | |
| color: #333; | |
| }} | |
| pre {{ | |
| background-color: #f4f4f4; | |
| padding: 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 5px; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Welcome to falcao-maps</h1> | |
| <p>Based on your uploaded dataset and deployed API, here are example API calls for your client:</p> | |
| <h2>1. Find Nearby Stores (JSON Response)</h2> | |
| <pre> | |
| /api/stores/nearby?lat=18.9695&lon=72.8320&radius=1 | |
| </pre> | |
| <p>Use this to get store details near Market Road area within 1km</p> | |
| <h2>2. View Basic Store Map</h2> | |
| <pre> | |
| /api/stores/map?lat=18.9701&lon=72.8330&radius=0.5 | |
| </pre> | |
| <p>Shows map centered at Main Street with 500m radius</p> | |
| <h2>3. View All Store Locations with Color Coding</h2> | |
| <pre> | |
| /api/stores/locations?lat=18.9685&lon=72.8325&radius=2 | |
| </pre> | |
| <p>Shows detailed map with color-coded stores within 2km</p> | |
| <h2>4. Get Route Between Points</h2> | |
| <p>Example routes:</p> | |
| <pre> | |
| # Route from Park Avenue to Hill Road stores (use simple visualization for memory optimization) | |
| /api/stores/route?user_lat=18.9710&user_lon=72.8335&store_lat=18.9705&store_lon=72.8345&viz_type=simple | |
| # Route from Main Street to Market Road stores | |
| /api/stores/route?user_lat=18.9701&user_lon=72.8330&store_lat=18.9695&store_lon=72.8320&viz_type=simple | |
| </pre> | |
| <h2>Key Location Points in Dataset:</h2> | |
| <ul> | |
| <li>Main Street Area: 18.9701, 72.8330</li> | |
| <li>Park Avenue: 18.9710, 72.8335</li> | |
| <li>Market Road: 18.9695, 72.8320</li> | |
| <li>Shopping Center: 18.9670, 72.8300</li> | |
| <li>Commercial Street: 18.9690, 72.8340</li> | |
| </ul> | |
| <h2>API Documentation</h2> | |
| <p>You can view the interactive API documentation at: <a href="/docs">/docs</a></p> | |
| </body> | |
| </html> | |
| """ | |
| return html_content | |
| async def get_nearby_stores( | |
| lat: float = Query(..., description="Latitude of user location"), | |
| lon: float = Query(..., description="Longitude of user location"), | |
| radius: float = Query(5.0, description="Search radius in kilometers") | |
| ): | |
| """Get nearby stores based on user location""" | |
| try: | |
| nearby_stores = store_locator.find_nearby_stores(lat, lon, radius) | |
| return {"status": "success", "stores": nearby_stores} | |
| except Exception as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| async def get_stores_map( | |
| lat: float = Query(..., description="Latitude of center point"), | |
| lon: float = Query(..., description="Longitude of center point"), | |
| radius: float = Query(5.0, description="Search radius in kilometers") | |
| ): | |
| """Get HTML map with store locations""" | |
| try: | |
| # Clean up temp files before creating new ones | |
| cleanup_temp_files() | |
| store_map = store_locator.create_store_map(lat, lon, radius) | |
| # Create complete HTML content | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> | |
| <title>Stores Map</title> | |
| <style> | |
| body {{ | |
| margin: 0; | |
| padding: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| }} | |
| #map {{ | |
| width: 100%; | |
| height: 100%; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| {store_map.get_root().render()} | |
| <script> | |
| window.onload = function() {{ | |
| setTimeout(function() {{ | |
| window.dispatchEvent(new Event('resize')); | |
| }}, 1000); | |
| }}; | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # Save the HTML to a file | |
| file_path = 'temp/stores_map.html' | |
| with open(file_path, 'w', encoding='utf-8') as f: | |
| f.write(html_content) | |
| # Return the file as HTML response | |
| return html_content | |
| except Exception as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| async def get_store_route( | |
| user_lat: float = Query(..., description="User location latitude"), | |
| user_lon: float = Query(..., description="User location longitude"), | |
| store_lat: float = Query(..., description="Store location latitude"), | |
| store_lon: float = Query(..., description="Store location longitude"), | |
| viz_type: str = Query("simple", description="Visualization type (simple or advanced)") | |
| ): | |
| """Get route between user and store locations with visualization""" | |
| try: | |
| # Clean up temp files before creating new ones | |
| cleanup_temp_files() | |
| # Initialize graph if not already initialized | |
| # Use a smaller distance to reduce memory usage | |
| if store_locator.network_graph is None: | |
| success = store_locator.initialize_graph((user_lat, user_lon), dist=10000) | |
| if not success: | |
| raise HTTPException(status_code=400, detail="Unable to initialize graph, try a different location") | |
| # Get nearest nodes | |
| start_node = ox.distance.nearest_nodes( | |
| store_locator.network_graph, user_lon, user_lat) | |
| end_node = ox.distance.nearest_nodes( | |
| store_locator.network_graph, store_lon, store_lat) | |
| try: | |
| # Calculate path using the travel_time weight | |
| path_time = nx.shortest_path( | |
| store_locator.network_graph, | |
| start_node, | |
| end_node, | |
| weight='travel_time' | |
| ) | |
| if viz_type == "simple": | |
| # Create a simple folium map for low-resource environments | |
| m = folium.Map( | |
| location=[(user_lat + store_lat) / 2, (user_lon + store_lon) / 2], | |
| zoom_start=15, | |
| tiles="cartodbpositron" | |
| ) | |
| # Add markers for start and end points | |
| folium.Marker( | |
| [user_lat, user_lon], | |
| popup='Your Location', | |
| icon=folium.Icon(color='green', icon='home') | |
| ).add_to(m) | |
| folium.Marker( | |
| [store_lat, store_lon], | |
| popup='Store Location', | |
| icon=folium.Icon(color='red', icon='info-sign') | |
| ).add_to(m) | |
| # Extract coordinates from the path | |
| path_coords = [] | |
| for node in path_time: | |
| x = store_locator.network_graph.nodes[node]['x'] | |
| y = store_locator.network_graph.nodes[node]['y'] | |
| path_coords.append([y, x]) # Note the y, x order for folium | |
| # Add the route line | |
| folium.PolyLine( | |
| locations=path_coords, | |
| weight=5, | |
| color='blue', | |
| opacity=0.7 | |
| ).add_to(m) | |
| # Add distance and time estimate | |
| total_distance = 0 | |
| total_time = 0 | |
| for i in range(len(path_time) - 1): | |
| a, b = path_time[i], path_time[i + 1] | |
| total_distance += store_locator.network_graph.edges[(a, b, 0)]['length'] | |
| total_time += store_locator.network_graph.edges[(a, b, 0)]['travel_time'] | |
| # Convert to km and minutes | |
| total_distance_km = round(total_distance / 1000, 2) | |
| total_time_min = round(total_time / 60, 1) | |
| # Add info box | |
| html_content = f""" | |
| <div style="position: fixed; top: 10px; left: 50px; z-index: 9999; | |
| background-color: white; padding: 10px; border-radius: 5px; | |
| box-shadow: 0 0 10px rgba(0,0,0,0.3);"> | |
| <h4 style="margin: 0 0 5px 0;">Route Information</h4> | |
| <p><b>Distance:</b> {total_distance_km} km<br> | |
| <b>Est. Time:</b> {total_time_min} minutes</p> | |
| </div> | |
| """ | |
| m.get_root().html.add_child(folium.Element(html_content)) | |
| # Add layer control | |
| folium.LayerControl().add_to(m) | |
| # Create complete HTML content | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> | |
| <title>Simple Route Map</title> | |
| <style> | |
| body {{ | |
| margin: 0; | |
| padding: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| }} | |
| #map {{ | |
| width: 100%; | |
| height: 100%; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| {m.get_root().render()} | |
| <script> | |
| window.onload = function() {{ | |
| setTimeout(function() {{ | |
| window.dispatchEvent(new Event('resize')); | |
| }}, 1000); | |
| }}; | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # Save the HTML to a file | |
| file_path = 'temp/simple_route_map.html' | |
| with open(file_path, 'w', encoding='utf-8') as f: | |
| f.write(html_content) | |
| # Return the HTML content | |
| return html_content | |
| else: | |
| # WARNING: Advanced visualization - may cause memory issues on limited resources | |
| # Limit the path nodes to reduce memory usage | |
| # Only include every Nth node | |
| step = max(1, len(path_time) // 30) # Maximum 30 points | |
| simplified_path = path_time[::step] | |
| if path_time[-1] not in simplified_path: | |
| simplified_path.append(path_time[-1]) | |
| # Create animation data (simplified) | |
| lst_start, lst_end = [], [] | |
| start_x, start_y = [], [] | |
| end_x, end_y = [], [] | |
| lst_length, lst_time = [], [] | |
| for a, b in zip(simplified_path[:-1], simplified_path[1:]): | |
| lst_start.append(a) | |
| lst_end.append(b) | |
| # Calculate accumulated length and time between simplified points | |
| segment_length = 0 | |
| segment_time = 0 | |
| path_segment = nx.shortest_path( | |
| store_locator.network_graph, a, b, weight='travel_time') | |
| for i in range(len(path_segment) - 1): | |
| u, v = path_segment[i], path_segment[i + 1] | |
| segment_length += store_locator.network_graph.edges[(u, v, 0)]['length'] | |
| segment_time += store_locator.network_graph.edges[(u, v, 0)]['travel_time'] | |
| lst_length.append(round(segment_length)) | |
| lst_time.append(round(segment_time)) | |
| start_x.append(store_locator.network_graph.nodes[a]['x']) | |
| start_y.append(store_locator.network_graph.nodes[a]['y']) | |
| end_x.append(store_locator.network_graph.nodes[b]['x']) | |
| end_y.append(store_locator.network_graph.nodes[b]['y']) | |
| df = pd.DataFrame( | |
| list(zip(lst_start, lst_end, start_x, start_y, end_x, end_y, | |
| lst_length, lst_time)), | |
| columns=["start", "end", "start_x", "start_y", | |
| "end_x", "end_y", "length", "travel_time"] | |
| ).reset_index().rename(columns={"index": "id"}) | |
| # Create animation using plotly (reduced complexity) | |
| df_start = df[df["start"] == lst_start[0]] | |
| df_end = df[df["end"] == lst_end[-1]] | |
| fig = px.scatter_mapbox( | |
| data_frame=df, | |
| lon="start_x", | |
| lat="start_y", | |
| zoom=15, | |
| width=800, # Reduced size | |
| height=600, # Reduced size | |
| animation_frame="id", | |
| mapbox_style="carto-positron" | |
| ) | |
| # Basic visualization elements only | |
| fig.data[0].marker = {"size": 12} | |
| # Add start point | |
| fig.add_trace( | |
| px.scatter_mapbox( | |
| data_frame=df_start, | |
| lon="start_x", | |
| lat="start_y" | |
| ).data[0] | |
| ) | |
| fig.data[1].marker = {"size": 15, "color": "red"} | |
| # Add end point | |
| fig.add_trace( | |
| px.scatter_mapbox( | |
| data_frame=df_end, | |
| lon="start_x", | |
| lat="start_y" | |
| ).data[0] | |
| ) | |
| fig.data[2].marker = {"size": 15, "color": "green"} | |
| # Add route | |
| fig.add_trace( | |
| px.line_mapbox( | |
| data_frame=df, | |
| lon="start_x", | |
| lat="start_y" | |
| ).data[0] | |
| ) | |
| # Simplified layout with fewer options to reduce complexity | |
| fig.update_layout( | |
| showlegend=False, | |
| margin={"r":0,"t":0,"l":0,"b":0}, | |
| autosize=True, | |
| height=None | |
| ) | |
| # Create complete HTML content | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> | |
| <title>Route Map</title> | |
| <style> | |
| body {{ | |
| margin: 0; | |
| padding: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| }} | |
| #map-container {{ | |
| width: 100%; | |
| height: 100%; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="map-container"> | |
| {fig.to_html(include_plotlyjs=True, full_html=False, config={'staticPlot': True})} | |
| </div> | |
| <script> | |
| window.onload = function() {{ | |
| setTimeout(function() {{ | |
| window.dispatchEvent(new Event('resize')); | |
| }}, 1000); | |
| }}; | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # Save the HTML to a file | |
| file_path = 'temp/route_map.html' | |
| with open(file_path, 'w', encoding='utf-8') as f: | |
| f.write(html_content) | |
| # Return the HTML content | |
| return html_content | |
| except nx.NetworkXNoPath: | |
| raise HTTPException(status_code=404, detail="No route found") | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| async def get_all_store_locations( | |
| lat: float = Query(..., description="Latitude of center point"), | |
| lon: float = Query(..., description="Longitude of center point"), | |
| radius: float = Query(10.0, description="Search radius in kilometers") | |
| ): | |
| """Get a map showing all stores in the given radius with colors based on distance""" | |
| try: | |
| # Clean up temp files before creating new ones | |
| cleanup_temp_files() | |
| # Get nearby stores | |
| nearby_stores = store_locator.find_nearby_stores(lat, lon, radius) | |
| # Limit number of stores for memory optimization | |
| max_stores = min(len(nearby_stores), 50) | |
| nearby_stores = nearby_stores[:max_stores] | |
| # Create base map centered on user location | |
| m = folium.Map( | |
| location=[lat, lon], | |
| zoom_start=12, | |
| tiles="cartodbpositron" | |
| ) | |
| # Add user location marker | |
| folium.Marker( | |
| [lat, lon], | |
| popup='Your Location', | |
| icon=folium.Icon(color='green', icon='home') | |
| ).add_to(m) | |
| # Add markers for each store with color coding based on distance | |
| for store in nearby_stores: | |
| # Color code based on distance | |
| if store['distance'] <= 2: | |
| color = 'red' # Very close | |
| elif store['distance'] <= 5: | |
| color = 'orange' # Moderate distance | |
| else: | |
| color = 'blue' # Further away | |
| # Create simplified popup content | |
| popup_content = f""" | |
| <div style='width: 200px; font-size: 14px;'> | |
| <h4 style='color: {color}; margin: 0 0 8px 0;'>{store['store_name']}</h4> | |
| <b>Distance:</b> {store['distance']} km<br> | |
| <b>Est. Delivery:</b> {store['estimated_delivery_time']} mins<br> | |
| <b>Categories:</b> {store['product_categories']}<br> | |
| <button onclick="window.location.href='/api/stores/route?user_lat={lat}&user_lon={lon}&store_lat={store['location']['lat']}&store_lon={store['location']['lon']}&viz_type=simple'" | |
| style='margin-top: 8px; padding: 8px; width: 100%; background-color: #007bff; color: white; border: none; border-radius: 4px;'> | |
| Get Route | |
| </button> | |
| </div> | |
| """ | |
| # Add store marker | |
| folium.Marker( | |
| location=[store['location']['lat'], store['location']['lon']], | |
| popup=folium.Popup(popup_content, max_width=300), | |
| icon=folium.Icon(color=color, icon='info-sign'), | |
| tooltip=f"{store['store_name']} ({store['distance']} km)" | |
| ).add_to(m) | |
| # Add circle to show distance - only for closer stores to reduce complexity | |
| if store['distance'] <= 5: | |
| folium.Circle( | |
| location=[store['location']['lat'], store['location']['lon']], | |
| radius=store['distance'] * 100, | |
| color=color, | |
| fill=True, | |
| opacity=0.1 | |
| ).add_to(m) | |
| # Add distance circles from user location - reduced to save memory | |
| for circle_radius, color in [(2000, 'red'), (5000, 'orange')]: | |
| folium.Circle( | |
| location=[lat, lon], | |
| radius=circle_radius, | |
| color=color, | |
| fill=False, | |
| weight=1,dash_array='5, 5' | |
| ).add_to(m) | |
| # Create mobile-friendly HTML content | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> | |
| <title>Nearby Stores</title> | |
| <style> | |
| body {{ | |
| margin: 0; | |
| padding: 0; | |
| width: 100vw; | |
| height: 100vh; | |
| overflow: hidden; | |
| }} | |
| #map {{ | |
| width: 100%; | |
| height: 100%; | |
| }} | |
| .legend {{ | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| background: white; | |
| padding: 10px; | |
| border-radius: 5px; | |
| box-shadow: 0 1px 5px rgba(0,0,0,0.2); | |
| font-size: 12px; | |
| z-index: 1000; | |
| }} | |
| .info-box {{ | |
| position: fixed; | |
| top: 20px; | |
| left: 20px; | |
| background: white; | |
| padding: 10px; | |
| border-radius: 5px; | |
| box-shadow: 0 1px 5px rgba(0,0,0,0.2); | |
| font-size: 12px; | |
| z-index: 1000; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| {m.get_root().render()} | |
| <div class="legend"> | |
| <b>Distance Zones</b><br> | |
| <span style="color: red;">β</span> < 2 km<br> | |
| <span style="color: orange;">β</span> 2-5 km<br> | |
| <span style="color: blue;">β</span> > 5 km | |
| </div> | |
| <div class="info-box"> | |
| <b>Search Radius:</b> {radius} km<br> | |
| <b>Stores Found:</b> {len(nearby_stores)} | |
| </div> | |
| <script> | |
| window.onload = function() {{ | |
| setTimeout(function() {{ | |
| window.dispatchEvent(new Event('resize')); | |
| }}, 1000); | |
| }}; | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| # Save and return the file | |
| file_path = 'temp/locations_map.html' | |
| with open(file_path, 'w', encoding='utf-8') as f: | |
| f.write(html_content) | |
| # Return the HTML content | |
| return html_content | |
| except Exception as e: | |
| raise HTTPException(status_code=400, detail=str(e)) | |
| # Add endpoint to serve static files directly | |
| async def get_temp_file(file_path: str): | |
| """Serve temporary files like HTML maps""" | |
| full_path = os.path.join("temp", file_path) | |
| if not os.path.exists(full_path): | |
| raise HTTPException(status_code=404, detail="File not found") | |
| return FileResponse(full_path) | |
| # Add memory monitoring and management middleware | |
| async def add_memory_management(request: Request, call_next): | |
| # Cleanup before processing request | |
| cleanup_temp_files() | |
| # Process the request | |
| response = await call_next(request) | |
| # Cleanup after processing request | |
| gc.collect() | |
| return response | |
| # For running the application directly (development mode) | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=8000) |