Zane Falcao commited on
Commit
4917185
Β·
1 Parent(s): 2a1e3e4
Files changed (2) hide show
  1. app.py +345 -409
  2. dapp.py +889 -0
app.py CHANGED
@@ -1,5 +1,5 @@
1
- from fastapi import FastAPI, Request, Query, HTTPException, Response
2
- from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
  import pandas as pd
5
  import numpy as np
@@ -16,12 +16,11 @@ import os
16
  import time
17
  from functools import lru_cache
18
  from rtree import index
19
- import gc
20
- import shutil
21
- from typing import Optional, List, Dict, Any, Union
22
  from pydantic import BaseModel, Field
 
23
 
24
- app = FastAPI(title="falcao-maps API", description="Store locator and route planning API")
 
25
 
26
  # Add CORS middleware
27
  app.add_middleware(
@@ -35,8 +34,11 @@ app.add_middleware(
35
  # Create temp directory for files
36
  os.makedirs('temp', exist_ok=True)
37
 
38
- # Custom JSON encoder for NumPy types
39
- class NumpyEncoder(json.JSONEncoder):
 
 
 
40
  def default(self, obj):
41
  if isinstance(obj, np.integer):
42
  return int(obj)
@@ -44,13 +46,10 @@ class NumpyEncoder(json.JSONEncoder):
44
  return float(obj)
45
  elif isinstance(obj, np.ndarray):
46
  return obj.tolist()
47
- return super(NumpyEncoder, self).default(obj)
48
 
49
- # Load and prepare the store data
50
- stores_df = pd.read_csv('dataset of 50 stores.csv')
51
-
52
- # Define Pydantic models for API responses
53
- class Location(BaseModel):
54
  lat: float
55
  lon: float
56
 
@@ -61,7 +60,7 @@ class Store(BaseModel):
61
  distance: float
62
  estimated_delivery_time: int
63
  product_categories: str
64
- location: Location
65
 
66
  class StoresResponse(BaseModel):
67
  status: str
@@ -70,7 +69,7 @@ class StoresResponse(BaseModel):
70
  class ErrorResponse(BaseModel):
71
  status: str
72
  message: str
73
-
74
  class StoreLocator:
75
  def __init__(self, stores_dataframe):
76
  self.stores_df = stores_dataframe
@@ -79,30 +78,16 @@ class StoreLocator:
79
  self.spatial_index = self._build_spatial_index()
80
 
81
  @lru_cache(maxsize=50)
82
- def initialize_graph(self, center_point, dist=30000): # Reduced distance for memory optimization
83
  """Initialize road network graph with caching"""
84
  cache_key = f"{center_point[0]}_{center_point[1]}"
85
  if cache_key in self.graph_cache:
86
  self.network_graph = self.graph_cache[cache_key]
87
  return True
88
  try:
89
- # Use simplify=True and increased tolerance for lower memory usage
90
- self.network_graph = ox.graph_from_point(
91
- center_point,
92
- dist=dist,
93
- network_type="drive",
94
- simplify=True,
95
- retain_all=False
96
- )
97
  self.network_graph = ox.add_edge_speeds(self.network_graph)
98
  self.network_graph = ox.add_edge_travel_times(self.network_graph)
99
-
100
- # Store in cache
101
- self.graph_cache[cache_key] = self.network_graph
102
-
103
- # Force garbage collection
104
- gc.collect()
105
-
106
  return True
107
  except Exception as e:
108
  print(f"Error initializing graph: {str(e)}")
@@ -137,7 +122,7 @@ class StoreLocator:
137
  multiplier = 1.0
138
 
139
  return round(base_minutes * multiplier)
140
-
141
  def find_nearby_stores(self, lat, lon, radius=5):
142
  """Find stores within radius using spatial index"""
143
  nearby_stores = []
@@ -167,24 +152,20 @@ class StoreLocator:
167
  return sorted(nearby_stores, key=lambda x: x['distance'])
168
 
169
  def create_store_map(self, center_lat, center_lon, radius=5):
170
- """Create an interactive map with store locations - optimized for memory"""
171
  # Create base map
172
- m = folium.Map(
173
- location=[center_lat, center_lon],
174
- zoom_start=13,
175
- tiles="cartodbpositron"
176
- )
177
 
178
  # Create marker cluster for better performance with many markers
179
  marker_cluster = plugins.MarkerCluster().add_to(m)
180
-
181
  # Add stores to map
182
  nearby_stores = self.find_nearby_stores(center_lat, center_lon, radius)
183
 
184
- # Limit the number of stores to reduce memory usage
185
- max_stores = min(len(nearby_stores), 50) # Cap at 50 stores
186
-
187
- for store in nearby_stores[:max_stores]:
188
  # Prepare popup content
189
  popup_content = f"""
190
  <div style='width: 200px'>
@@ -203,15 +184,14 @@ class StoreLocator:
203
  icon=folium.Icon(color='red', icon='info-sign')
204
  ).add_to(marker_cluster)
205
 
206
- # Add line to show distance from center (only for closer stores)
207
- if store['distance'] <= nearby_stores[min(9, len(nearby_stores)-1)]['distance']:
208
- folium.PolyLine(
209
- locations=[[center_lat, center_lon],
210
- [store['location']['lat'], store['location']['lon']]],
211
- weight=2,
212
- color='blue',
213
- opacity=0.3
214
- ).add_to(m)
215
 
216
  # Add current location marker
217
  folium.Marker(
@@ -220,16 +200,84 @@ class StoreLocator:
220
  icon=folium.Icon(color='green', icon='home')
221
  ).add_to(m)
222
 
223
- # Add layer control only
 
 
 
224
  folium.LayerControl().add_to(m)
225
 
226
  return m
227
-
228
- # Initialize store locator
229
  store_locator = StoreLocator(stores_df)
230
 
231
- # Helper functions for cleaning temporary files
232
- def cleanup_temp_files():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  temp_dir = 'temp'
234
  if os.path.exists(temp_dir):
235
  for file in os.listdir(temp_dir):
@@ -241,24 +289,13 @@ def cleanup_temp_files():
241
  os.remove(file_path)
242
  except Exception as e:
243
  print(f"Error cleaning up temp files: {e}")
 
 
 
244
 
245
- # Register cleanup on startup and shutdown
246
- @app.on_event("startup")
247
- async def startup_event():
248
- cleanup_temp_files()
249
-
250
- @app.on_event("shutdown")
251
- async def shutdown_event():
252
- # Clean up all temporary files on shutdown
253
- try:
254
- shutil.rmtree('temp')
255
- except Exception as e:
256
- print(f"Error cleaning up temp directory: {e}")
257
-
258
- # Routes
259
  @app.get("/", response_class=HTMLResponse)
260
  async def home():
261
- """API Documentation Homepage"""
262
  html_content = f"""
263
  <!DOCTYPE html>
264
  <html lang="en">
@@ -288,30 +325,30 @@ async def home():
288
 
289
  <h2>1. Find Nearby Stores (JSON Response)</h2>
290
  <pre>
291
- /api/stores/nearby?lat=18.9695&lon=72.8320&radius=1
292
  </pre>
293
  <p>Use this to get store details near Market Road area within 1km</p>
294
 
295
  <h2>2. View Basic Store Map</h2>
296
  <pre>
297
- /api/stores/map?lat=18.9701&lon=72.8330&radius=0.5
298
  </pre>
299
  <p>Shows map centered at Main Street with 500m radius</p>
300
 
301
  <h2>3. View All Store Locations with Color Coding</h2>
302
  <pre>
303
- /api/stores/locations?lat=18.9685&lon=72.8325&radius=2
304
  </pre>
305
  <p>Shows detailed map with color-coded stores within 2km</p>
306
 
307
  <h2>4. Get Route Between Points</h2>
308
  <p>Example routes:</p>
309
  <pre>
310
- # Route from Park Avenue to Hill Road stores (use simple visualization for memory optimization)
311
- /api/stores/route?user_lat=18.9710&user_lon=72.8335&store_lat=18.9705&store_lon=72.8345&viz_type=simple
312
 
313
  # Route from Main Street to Market Road stores
314
- /api/stores/route?user_lat=18.9701&user_lon=72.8330&store_lat=18.9695&store_lon=72.8320&viz_type=simple
315
  </pre>
316
 
317
  <h2>Key Location Points in Dataset:</h2>
@@ -322,21 +359,20 @@ async def home():
322
  <li>Shopping Center: 18.9670, 72.8300</li>
323
  <li>Commercial Street: 18.9690, 72.8340</li>
324
  </ul>
325
-
326
- <h2>API Documentation</h2>
327
- <p>You can view the interactive API documentation at: <a href="/docs">/docs</a></p>
328
  </body>
329
  </html>
330
  """
331
- return html_content
332
 
333
  @app.get("/api/stores/nearby", response_model=StoresResponse, responses={400: {"model": ErrorResponse}})
334
  async def get_nearby_stores(
335
  lat: float = Query(..., description="Latitude of user location"),
336
  lon: float = Query(..., description="Longitude of user location"),
337
- radius: float = Query(5.0, description="Search radius in kilometers")
338
  ):
339
- """Get nearby stores based on user location"""
 
 
340
  try:
341
  nearby_stores = store_locator.find_nearby_stores(lat, lon, radius)
342
  return {"status": "success", "stores": nearby_stores}
@@ -347,13 +383,12 @@ async def get_nearby_stores(
347
  async def get_stores_map(
348
  lat: float = Query(..., description="Latitude of center point"),
349
  lon: float = Query(..., description="Longitude of center point"),
350
- radius: float = Query(5.0, description="Search radius in kilometers")
351
  ):
352
- """Get HTML map with store locations"""
 
 
353
  try:
354
- # Clean up temp files before creating new ones
355
- cleanup_temp_files()
356
-
357
  store_map = store_locator.create_store_map(lat, lon, radius)
358
 
359
  # Create complete HTML content
@@ -396,31 +431,26 @@ async def get_stores_map(
396
  with open(file_path, 'w', encoding='utf-8') as f:
397
  f.write(html_content)
398
 
399
- # Return the file as HTML response
400
- return html_content
401
 
402
  except Exception as e:
403
  raise HTTPException(status_code=400, detail=str(e))
404
-
405
  @app.get("/api/stores/route", response_class=HTMLResponse, responses={400: {"model": ErrorResponse}, 404: {"model": ErrorResponse}})
406
  async def get_store_route(
407
- user_lat: float = Query(..., description="User location latitude"),
408
- user_lon: float = Query(..., description="User location longitude"),
409
- store_lat: float = Query(..., description="Store location latitude"),
410
- store_lon: float = Query(..., description="Store location longitude"),
411
- viz_type: str = Query("simple", description="Visualization type (simple or advanced)")
412
  ):
413
- """Get route between user and store locations with visualization"""
 
 
414
  try:
415
- # Clean up temp files before creating new ones
416
- cleanup_temp_files()
417
-
418
  # Initialize graph if not already initialized
419
- # Use a smaller distance to reduce memory usage
420
  if store_locator.network_graph is None:
421
- success = store_locator.initialize_graph((user_lat, user_lon), dist=10000)
422
- if not success:
423
- raise HTTPException(status_code=400, detail="Unable to initialize graph, try a different location")
424
 
425
  # Get nearest nodes
426
  start_node = ox.distance.nearest_nodes(
@@ -429,7 +459,7 @@ async def get_store_route(
429
  store_locator.network_graph, store_lon, store_lat)
430
 
431
  try:
432
- # Calculate path using the travel_time weight
433
  path_time = nx.shortest_path(
434
  store_locator.network_graph,
435
  start_node,
@@ -437,285 +467,206 @@ async def get_store_route(
437
  weight='travel_time'
438
  )
439
 
440
- if viz_type == "simple":
441
- # Create a simple folium map for low-resource environments
442
- m = folium.Map(
443
- location=[(user_lat + store_lat) / 2, (user_lon + store_lon) / 2],
444
- zoom_start=15,
445
- tiles="cartodbpositron"
446
- )
447
-
448
- # Add markers for start and end points
449
- folium.Marker(
450
- [user_lat, user_lon],
451
- popup='Your Location',
452
- icon=folium.Icon(color='green', icon='home')
453
- ).add_to(m)
454
-
455
- folium.Marker(
456
- [store_lat, store_lon],
457
- popup='Store Location',
458
- icon=folium.Icon(color='red', icon='info-sign')
459
- ).add_to(m)
460
-
461
- # Extract coordinates from the path
462
- path_coords = []
463
- for node in path_time:
464
- x = store_locator.network_graph.nodes[node]['x']
465
- y = store_locator.network_graph.nodes[node]['y']
466
- path_coords.append([y, x]) # Note the y, x order for folium
467
-
468
- # Add the route line
469
- folium.PolyLine(
470
- locations=path_coords,
471
- weight=5,
472
- color='blue',
473
- opacity=0.7
474
- ).add_to(m)
475
-
476
- # Add distance and time estimate
477
- total_distance = 0
478
- total_time = 0
479
-
480
- for i in range(len(path_time) - 1):
481
- a, b = path_time[i], path_time[i + 1]
482
- total_distance += store_locator.network_graph.edges[(a, b, 0)]['length']
483
- total_time += store_locator.network_graph.edges[(a, b, 0)]['travel_time']
484
-
485
- # Convert to km and minutes
486
- total_distance_km = round(total_distance / 1000, 2)
487
- total_time_min = round(total_time / 60, 1)
488
-
489
- # Add info box
490
- html_content = f"""
491
- <div style="position: fixed; top: 10px; left: 50px; z-index: 9999;
492
- background-color: white; padding: 10px; border-radius: 5px;
493
- box-shadow: 0 0 10px rgba(0,0,0,0.3);">
494
- <h4 style="margin: 0 0 5px 0;">Route Information</h4>
495
- <p><b>Distance:</b> {total_distance_km} km<br>
496
- <b>Est. Time:</b> {total_time_min} minutes</p>
497
- </div>
498
- """
499
-
500
- m.get_root().html.add_child(folium.Element(html_content))
501
-
502
- # Add layer control
503
- folium.LayerControl().add_to(m)
504
-
505
- # Create complete HTML content
506
- html_content = f"""
507
- <!DOCTYPE html>
508
- <html>
509
- <head>
510
- <meta charset="utf-8">
511
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
512
- <title>Simple Route Map</title>
513
- <style>
514
- body {{
515
- margin: 0;
516
- padding: 0;
517
- width: 100vw;
518
- height: 100vh;
519
- overflow: hidden;
520
- }}
521
- #map {{
522
- width: 100%;
523
- height: 100%;
524
- }}
525
- </style>
526
- </head>
527
- <body>
528
- {m.get_root().render()}
529
- <script>
530
- window.onload = function() {{
531
- setTimeout(function() {{
532
- window.dispatchEvent(new Event('resize'));
533
- }}, 1000);
534
- }};
535
- </script>
536
- </body>
537
- </html>
538
- """
539
-
540
- # Save the HTML to a file
541
- file_path = 'temp/simple_route_map.html'
542
- with open(file_path, 'w', encoding='utf-8') as f:
543
- f.write(html_content)
544
-
545
- # Return the HTML content
546
- return html_content
547
-
548
- else:
549
- # WARNING: Advanced visualization - may cause memory issues on limited resources
550
-
551
- # Limit the path nodes to reduce memory usage
552
- # Only include every Nth node
553
- step = max(1, len(path_time) // 30) # Maximum 30 points
554
- simplified_path = path_time[::step]
555
- if path_time[-1] not in simplified_path:
556
- simplified_path.append(path_time[-1])
557
-
558
- # Create animation data (simplified)
559
- lst_start, lst_end = [], []
560
- start_x, start_y = [], []
561
- end_x, end_y = [], []
562
- lst_length, lst_time = [], []
563
-
564
- for a, b in zip(simplified_path[:-1], simplified_path[1:]):
565
- lst_start.append(a)
566
- lst_end.append(b)
567
-
568
- # Calculate accumulated length and time between simplified points
569
- segment_length = 0
570
- segment_time = 0
571
- path_segment = nx.shortest_path(
572
- store_locator.network_graph, a, b, weight='travel_time')
573
-
574
- for i in range(len(path_segment) - 1):
575
- u, v = path_segment[i], path_segment[i + 1]
576
- segment_length += store_locator.network_graph.edges[(u, v, 0)]['length']
577
- segment_time += store_locator.network_graph.edges[(u, v, 0)]['travel_time']
578
-
579
- lst_length.append(round(segment_length))
580
- lst_time.append(round(segment_time))
581
- start_x.append(store_locator.network_graph.nodes[a]['x'])
582
- start_y.append(store_locator.network_graph.nodes[a]['y'])
583
- end_x.append(store_locator.network_graph.nodes[b]['x'])
584
- end_y.append(store_locator.network_graph.nodes[b]['y'])
585
-
586
- df = pd.DataFrame(
587
- list(zip(lst_start, lst_end, start_x, start_y, end_x, end_y,
588
- lst_length, lst_time)),
589
- columns=["start", "end", "start_x", "start_y",
590
- "end_x", "end_y", "length", "travel_time"]
591
- ).reset_index().rename(columns={"index": "id"})
592
-
593
- # Create animation using plotly (reduced complexity)
594
- df_start = df[df["start"] == lst_start[0]]
595
- df_end = df[df["end"] == lst_end[-1]]
596
-
597
- fig = px.scatter_mapbox(
598
  data_frame=df,
599
  lon="start_x",
600
- lat="start_y",
601
- zoom=15,
602
- width=800, # Reduced size
603
- height=600, # Reduced size
604
- animation_frame="id",
605
- mapbox_style="carto-positron"
606
- )
607
-
608
- # Basic visualization elements only
609
- fig.data[0].marker = {"size": 12}
610
-
611
- # Add start point
612
- fig.add_trace(
613
- px.scatter_mapbox(
614
- data_frame=df_start,
615
- lon="start_x",
616
- lat="start_y"
617
- ).data[0]
618
- )
619
- fig.data[1].marker = {"size": 15, "color": "red"}
620
-
621
- # Add end point
622
- fig.add_trace(
623
- px.scatter_mapbox(
624
- data_frame=df_end,
625
- lon="start_x",
626
- lat="start_y"
627
- ).data[0]
628
- )
629
- fig.data[2].marker = {"size": 15, "color": "green"}
630
-
631
- # Add route
632
- fig.add_trace(
633
- px.line_mapbox(
634
- data_frame=df,
635
- lon="start_x",
636
- lat="start_y"
637
- ).data[0]
638
- )
639
-
640
- # Simplified layout with fewer options to reduce complexity
641
- fig.update_layout(
642
- showlegend=False,
643
- margin={"r":0,"t":0,"l":0,"b":0},
644
- autosize=True,
645
- height=None
646
- )
647
-
648
- # Create complete HTML content
649
- html_content = f"""
650
- <!DOCTYPE html>
651
- <html>
652
- <head>
653
- <meta charset="utf-8">
654
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
655
- <title>Route Map</title>
656
- <style>
657
- body {{
658
- margin: 0;
659
- padding: 0;
660
- width: 100vw;
661
- height: 100vh;
662
- overflow: hidden;
663
- }}
664
- #map-container {{
665
- width: 100%;
666
- height: 100%;
667
- }}
668
- </style>
669
- </head>
670
- <body>
671
- <div id="map-container">
672
- {fig.to_html(include_plotlyjs=True, full_html=False, config={'staticPlot': True})}
673
- </div>
674
- <script>
675
- window.onload = function() {{
676
- setTimeout(function() {{
677
- window.dispatchEvent(new Event('resize'));
678
- }}, 1000);
679
- }};
680
- </script>
681
- </body>
682
- </html>
683
- """
684
-
685
- # Save the HTML to a file
686
- file_path = 'temp/route_map.html'
687
- with open(file_path, 'w', encoding='utf-8') as f:
688
- f.write(html_content)
689
-
690
- # Return the HTML content
691
- return html_content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
 
693
  except nx.NetworkXNoPath:
694
  raise HTTPException(status_code=404, detail="No route found")
695
 
696
- except HTTPException:
697
- raise
698
  except Exception as e:
699
  raise HTTPException(status_code=400, detail=str(e))
700
-
701
  @app.get("/api/stores/locations", response_class=HTMLResponse, responses={400: {"model": ErrorResponse}})
702
  async def get_all_store_locations(
703
  lat: float = Query(..., description="Latitude of center point"),
704
  lon: float = Query(..., description="Longitude of center point"),
705
- radius: float = Query(10.0, description="Search radius in kilometers")
706
  ):
707
- """Get a map showing all stores in the given radius with colors based on distance"""
 
 
708
  try:
709
- # Clean up temp files before creating new ones
710
- cleanup_temp_files()
711
-
712
  # Get nearby stores
713
  nearby_stores = store_locator.find_nearby_stores(lat, lon, radius)
714
 
715
- # Limit number of stores for memory optimization
716
- max_stores = min(len(nearby_stores), 50)
717
- nearby_stores = nearby_stores[:max_stores]
718
-
719
  # Create base map centered on user location
720
  m = folium.Map(
721
  location=[lat, lon],
@@ -740,14 +691,16 @@ async def get_all_store_locations(
740
  else:
741
  color = 'blue' # Further away
742
 
743
- # Create simplified popup content
744
  popup_content = f"""
745
  <div style='width: 200px; font-size: 14px;'>
746
  <h4 style='color: {color}; margin: 0 0 8px 0;'>{store['store_name']}</h4>
 
747
  <b>Distance:</b> {store['distance']} km<br>
748
  <b>Est. Delivery:</b> {store['estimated_delivery_time']} mins<br>
 
749
  <b>Categories:</b> {store['product_categories']}<br>
750
- <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'"
751
  style='margin-top: 8px; padding: 8px; width: 100%; background-color: #007bff; color: white; border: none; border-radius: 4px;'>
752
  Get Route
753
  </button>
@@ -762,24 +715,24 @@ async def get_all_store_locations(
762
  tooltip=f"{store['store_name']} ({store['distance']} km)"
763
  ).add_to(m)
764
 
765
- # Add circle to show distance - only for closer stores to reduce complexity
766
- if store['distance'] <= 5:
767
- folium.Circle(
768
- location=[store['location']['lat'], store['location']['lon']],
769
- radius=store['distance'] * 100,
770
- color=color,
771
- fill=True,
772
- opacity=0.1
773
- ).add_to(m)
774
 
775
- # Add distance circles from user location - reduced to save memory
776
- for circle_radius, color in [(2000, 'red'), (5000, 'orange')]:
777
  folium.Circle(
778
  location=[lat, lon],
779
- radius=circle_radius,
780
  color=color,
781
  fill=False,
782
- weight=1,dash_array='5, 5'
 
783
  ).add_to(m)
784
 
785
  # Create mobile-friendly HTML content
@@ -854,36 +807,19 @@ async def get_all_store_locations(
854
  with open(file_path, 'w', encoding='utf-8') as f:
855
  f.write(html_content)
856
 
857
- # Return the HTML content
858
- return html_content
859
 
860
  except Exception as e:
861
  raise HTTPException(status_code=400, detail=str(e))
862
 
863
- # Add endpoint to serve static files directly
864
- @app.get("/temp/{file_path:path}", response_class=FileResponse)
865
- async def get_temp_file(file_path: str):
866
- """Serve temporary files like HTML maps"""
867
- full_path = os.path.join("temp", file_path)
868
- if not os.path.exists(full_path):
869
- raise HTTPException(status_code=404, detail="File not found")
870
- return FileResponse(full_path)
871
-
872
- # Add memory monitoring and management middleware
873
- @app.middleware("http")
874
- async def add_memory_management(request: Request, call_next):
875
- # Cleanup before processing request
876
- cleanup_temp_files()
877
-
878
- # Process the request
879
- response = await call_next(request)
880
-
881
- # Cleanup after processing request
882
- gc.collect()
883
-
884
- return response
885
 
886
- # For running the application directly (development mode)
887
  if __name__ == "__main__":
888
  import uvicorn
889
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
1
+ from fastapi import FastAPI, HTTPException, Query, Request, Response
2
+ from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
  import pandas as pd
5
  import numpy as np
 
16
  import time
17
  from functools import lru_cache
18
  from rtree import index
 
 
 
19
  from pydantic import BaseModel, Field
20
+ from typing import List, Dict, Any, Optional
21
 
22
+ # Create app instance
23
+ app = FastAPI(title="Store Locator API")
24
 
25
  # Add CORS middleware
26
  app.add_middleware(
 
34
  # Create temp directory for files
35
  os.makedirs('temp', exist_ok=True)
36
 
37
+ # Load and prepare the store data
38
+ stores_df = pd.read_csv('dataset of 50 stores.csv')
39
+
40
+ # Custom JSON encoder for numpy types
41
+ class NumpyJSONEncoder(json.JSONEncoder):
42
  def default(self, obj):
43
  if isinstance(obj, np.integer):
44
  return int(obj)
 
46
  return float(obj)
47
  elif isinstance(obj, np.ndarray):
48
  return obj.tolist()
49
+ return super().default(obj)
50
 
51
+ # Pydantic models for response validation
52
+ class StoreLocation(BaseModel):
 
 
 
53
  lat: float
54
  lon: float
55
 
 
60
  distance: float
61
  estimated_delivery_time: int
62
  product_categories: str
63
+ location: StoreLocation
64
 
65
  class StoresResponse(BaseModel):
66
  status: str
 
69
  class ErrorResponse(BaseModel):
70
  status: str
71
  message: str
72
+
73
  class StoreLocator:
74
  def __init__(self, stores_dataframe):
75
  self.stores_df = stores_dataframe
 
78
  self.spatial_index = self._build_spatial_index()
79
 
80
  @lru_cache(maxsize=50)
81
+ def initialize_graph(self, center_point, dist=20000):
82
  """Initialize road network graph with caching"""
83
  cache_key = f"{center_point[0]}_{center_point[1]}"
84
  if cache_key in self.graph_cache:
85
  self.network_graph = self.graph_cache[cache_key]
86
  return True
87
  try:
88
+ self.network_graph = ox.graph_from_point(center_point, dist=dist, network_type="drive")
 
 
 
 
 
 
 
89
  self.network_graph = ox.add_edge_speeds(self.network_graph)
90
  self.network_graph = ox.add_edge_travel_times(self.network_graph)
 
 
 
 
 
 
 
91
  return True
92
  except Exception as e:
93
  print(f"Error initializing graph: {str(e)}")
 
122
  multiplier = 1.0
123
 
124
  return round(base_minutes * multiplier)
125
+
126
  def find_nearby_stores(self, lat, lon, radius=5):
127
  """Find stores within radius using spatial index"""
128
  nearby_stores = []
 
152
  return sorted(nearby_stores, key=lambda x: x['distance'])
153
 
154
  def create_store_map(self, center_lat, center_lon, radius=5):
155
+ """Create an interactive map with store locations"""
156
  # Create base map
157
+ m = folium.Map(location=[center_lat, center_lon],
158
+ zoom_start=13,
159
+ tiles="cartodbpositron",
160
+ prefer_canvas=True
161
+ )
162
 
163
  # Create marker cluster for better performance with many markers
164
  marker_cluster = plugins.MarkerCluster().add_to(m)
 
165
  # Add stores to map
166
  nearby_stores = self.find_nearby_stores(center_lat, center_lon, radius)
167
 
168
+ for store in nearby_stores:
 
 
 
169
  # Prepare popup content
170
  popup_content = f"""
171
  <div style='width: 200px'>
 
184
  icon=folium.Icon(color='red', icon='info-sign')
185
  ).add_to(marker_cluster)
186
 
187
+ # Add line to show distance from center
188
+ folium.PolyLine(
189
+ locations=[[center_lat, center_lon],
190
+ [store['location']['lat'], store['location']['lon']]],
191
+ weight=2,
192
+ color='blue',
193
+ opacity=0.3
194
+ ).add_to(m)
 
195
 
196
  # Add current location marker
197
  folium.Marker(
 
200
  icon=folium.Icon(color='green', icon='home')
201
  ).add_to(m)
202
 
203
+ # Add fullscreen option
204
+ plugins.Fullscreen().add_to(m)
205
+
206
+ # Add layer control
207
  folium.LayerControl().add_to(m)
208
 
209
  return m
210
+
211
+ # Initialize store locator
212
  store_locator = StoreLocator(stores_df)
213
 
214
+ def create_animated_route(G, path, color, weight=3):
215
+ """Create an animated route visualization"""
216
+ features = []
217
+ timestamps = []
218
+
219
+ # Convert path nodes to coordinates
220
+ route_coords = [
221
+ (G.nodes[node]['y'], G.nodes[node]['x'])
222
+ for node in path
223
+ ]
224
+
225
+ # Create features for each segment of the route
226
+ for i in range(len(route_coords) - 1):
227
+ segment = {
228
+ 'type': 'Feature',
229
+ 'geometry': {
230
+ 'type': 'LineString',
231
+ 'coordinates': [
232
+ [route_coords[i][1], route_coords[i][0]],
233
+ [route_coords[i+1][1], route_coords[i+1][0]]
234
+ ]
235
+ },
236
+ 'properties': {
237
+ 'times': [datetime.now().isoformat()],
238
+ 'style': {
239
+ 'color': color,
240
+ 'weight': weight,
241
+ 'opacity': 0.8
242
+ }
243
+ }
244
+ }
245
+ features.append(segment)
246
+ timestamps.append(datetime.now().isoformat())
247
+
248
+ return features
249
+
250
+ def create_route_animation_data(G, path_time, path_length):
251
+ """Create animation data for route visualization"""
252
+ lst_start, lst_end = [], []
253
+ start_x, start_y = [], []
254
+ end_x, end_y = [], []
255
+ lst_length, lst_time = [], []
256
+
257
+ # Process time-based path
258
+ for a, b in zip(path_time[:-1], path_time[1:]):
259
+ lst_start.append(a)
260
+ lst_end.append(b)
261
+ lst_length.append(round(G.edges[(a,b,0)]['length']))
262
+ lst_time.append(round(G.edges[(a,b,0)]['travel_time']))
263
+ start_x.append(G.nodes[a]['x'])
264
+ start_y.append(G.nodes[a]['y'])
265
+ end_x.append(G.nodes[b]['x'])
266
+ end_y.append(G.nodes[b]['y'])
267
+
268
+ # Create DataFrame
269
+ df = pd.DataFrame(
270
+ list(zip(lst_start, lst_end, start_x, start_y, end_x, end_y,
271
+ lst_length, lst_time)),
272
+ columns=["start", "end", "start_x", "start_y", "end_x", "end_y",
273
+ "length", "travel_time"]
274
+ ).reset_index().rename(columns={"index": "id"})
275
+
276
+ return df
277
+
278
+ # Function to clean up temporary files
279
+ @app.middleware("http")
280
+ async def cleanup_temp_files(request: Request, call_next):
281
  temp_dir = 'temp'
282
  if os.path.exists(temp_dir):
283
  for file in os.listdir(temp_dir):
 
289
  os.remove(file_path)
290
  except Exception as e:
291
  print(f"Error cleaning up temp files: {e}")
292
+
293
+ response = await call_next(request)
294
+ return response
295
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  @app.get("/", response_class=HTMLResponse)
297
  async def home():
298
+ """API documentation homepage"""
299
  html_content = f"""
300
  <!DOCTYPE html>
301
  <html lang="en">
 
325
 
326
  <h2>1. Find Nearby Stores (JSON Response)</h2>
327
  <pre>
328
+ https://maps-yiv5.onrender.com/api/stores/nearby?lat=18.9695&lon=72.8320&radius=1
329
  </pre>
330
  <p>Use this to get store details near Market Road area within 1km</p>
331
 
332
  <h2>2. View Basic Store Map</h2>
333
  <pre>
334
+ https://maps-yiv5.onrender.com/api/stores/map?lat=18.9701&lon=72.8330&radius=0.5
335
  </pre>
336
  <p>Shows map centered at Main Street with 500m radius</p>
337
 
338
  <h2>3. View All Store Locations with Color Coding</h2>
339
  <pre>
340
+ https://maps-yiv5.onrender.com/api/stores/locations?lat=18.9685&lon=72.8325&radius=2
341
  </pre>
342
  <p>Shows detailed map with color-coded stores within 2km</p>
343
 
344
  <h2>4. Get Route Between Points</h2>
345
  <p>Example routes:</p>
346
  <pre>
347
+ # Route from Park Avenue to Hill Road stores
348
+ https://maps-yiv5.onrender.com/api/stores/route?user_lat=18.9710&user_lon=72.8335&store_lat=18.9705&store_lon=72.8345&viz_type=simple
349
 
350
  # Route from Main Street to Market Road stores
351
+ https://maps-yiv5.onrender.com/api/stores/route?user_lat=18.9701&user_lon=72.8330&store_lat=18.9695&store_lon=72.8320&viz_type=simple
352
  </pre>
353
 
354
  <h2>Key Location Points in Dataset:</h2>
 
359
  <li>Shopping Center: 18.9670, 72.8300</li>
360
  <li>Commercial Street: 18.9690, 72.8340</li>
361
  </ul>
 
 
 
362
  </body>
363
  </html>
364
  """
365
+ return HTMLResponse(content=html_content)
366
 
367
  @app.get("/api/stores/nearby", response_model=StoresResponse, responses={400: {"model": ErrorResponse}})
368
  async def get_nearby_stores(
369
  lat: float = Query(..., description="Latitude of user location"),
370
  lon: float = Query(..., description="Longitude of user location"),
371
+ radius: float = Query(5, description="Search radius in kilometers")
372
  ):
373
+ """
374
+ Get nearby stores based on user location
375
+ """
376
  try:
377
  nearby_stores = store_locator.find_nearby_stores(lat, lon, radius)
378
  return {"status": "success", "stores": nearby_stores}
 
383
  async def get_stores_map(
384
  lat: float = Query(..., description="Latitude of center point"),
385
  lon: float = Query(..., description="Longitude of center point"),
386
+ radius: float = Query(5, description="Search radius in kilometers")
387
  ):
388
+ """
389
+ Get HTML map with store locations
390
+ """
391
  try:
 
 
 
392
  store_map = store_locator.create_store_map(lat, lon, radius)
393
 
394
  # Create complete HTML content
 
431
  with open(file_path, 'w', encoding='utf-8') as f:
432
  f.write(html_content)
433
 
434
+ # Return the file
435
+ return FileResponse(file_path, media_type='text/html')
436
 
437
  except Exception as e:
438
  raise HTTPException(status_code=400, detail=str(e))
439
+
440
  @app.get("/api/stores/route", response_class=HTMLResponse, responses={400: {"model": ErrorResponse}, 404: {"model": ErrorResponse}})
441
  async def get_store_route(
442
+ user_lat: float = Query(..., description="Latitude of user location"),
443
+ user_lon: float = Query(..., description="Longitude of user location"),
444
+ store_lat: float = Query(..., description="Latitude of store location"),
445
+ store_lon: float = Query(..., description="Longitude of store location")
 
446
  ):
447
+ """
448
+ Get route between user location and store
449
+ """
450
  try:
 
 
 
451
  # Initialize graph if not already initialized
 
452
  if store_locator.network_graph is None:
453
+ store_locator.initialize_graph((user_lat, user_lon))
 
 
454
 
455
  # Get nearest nodes
456
  start_node = ox.distance.nearest_nodes(
 
459
  store_locator.network_graph, store_lon, store_lat)
460
 
461
  try:
462
+ # Calculate paths
463
  path_time = nx.shortest_path(
464
  store_locator.network_graph,
465
  start_node,
 
467
  weight='travel_time'
468
  )
469
 
470
+ # Create animation data
471
+ lst_start, lst_end = [], []
472
+ start_x, start_y = [], []
473
+ end_x, end_y = [], []
474
+ lst_length, lst_time = [], []
475
+
476
+ for a, b in zip(path_time[:-1], path_time[1:]):
477
+ lst_start.append(a)
478
+ lst_end.append(b)
479
+ lst_length.append(round(store_locator.network_graph.edges[(a,b,0)]['length']))
480
+ lst_time.append(round(store_locator.network_graph.edges[(a,b,0)]['travel_time']))
481
+ start_x.append(store_locator.network_graph.nodes[a]['x'])
482
+ start_y.append(store_locator.network_graph.nodes[a]['y'])
483
+ end_x.append(store_locator.network_graph.nodes[b]['x'])
484
+ end_y.append(store_locator.network_graph.nodes[b]['y'])
485
+
486
+ df = pd.DataFrame(
487
+ list(zip(lst_start, lst_end, start_x, start_y, end_x, end_y,
488
+ lst_length, lst_time)),
489
+ columns=["start", "end", "start_x", "start_y",
490
+ "end_x", "end_y", "length", "travel_time"]
491
+ ).reset_index().rename(columns={"index": "id"})
492
+
493
+ # Create animation using plotly
494
+ df_start = df[df["start"] == start_node]
495
+ df_end = df[df["end"] == end_node]
496
+
497
+ fig = px.scatter_mapbox(
498
+ data_frame=df,
499
+ lon="start_x",
500
+ lat="start_y",
501
+ zoom=15,
502
+ width=1000,
503
+ height=800,
504
+ animation_frame="id",
505
+ mapbox_style="carto-positron"
506
+ )
507
+
508
+ # Add driver marker
509
+ fig.data[0].marker = {"size": 12}
510
+
511
+ # Add start point
512
+ fig.add_trace(
513
+ px.scatter_mapbox(
514
+ data_frame=df_start,
515
+ lon="start_x",
516
+ lat="start_y"
517
+ ).data[0]
518
+ )
519
+ fig.data[1].marker = {"size": 15, "color": "red"}
520
+
521
+ # Add end point
522
+ fig.add_trace(
523
+ px.scatter_mapbox(
524
+ data_frame=df_end,
525
+ lon="start_x",
526
+ lat="start_y"
527
+ ).data[0]
528
+ )
529
+ fig.data[2].marker = {"size": 15, "color": "green"}
530
+
531
+ # Add route
532
+ fig.add_trace(
533
+ px.line_mapbox(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  data_frame=df,
535
  lon="start_x",
536
+ lat="start_y"
537
+ ).data[0]
538
+ )
539
+
540
+ # Update layout with slower animation settings
541
+ fig.update_layout(
542
+ showlegend=False,
543
+ margin={"r":0,"t":0,"l":0,"b":0},
544
+ autosize=True,
545
+ height=None,
546
+ updatemenus=[{
547
+ "type": "buttons",
548
+ "showactive": False,
549
+ "y": 0,
550
+ "x": 0,
551
+ "xanchor": "left",
552
+ "yanchor": "bottom",
553
+ "buttons": [
554
+ {
555
+ "label": "Play",
556
+ "method": "animate",
557
+ "args": [
558
+ None,
559
+ {
560
+ "frame": {"duration": 1000, "redraw": True},
561
+ "fromcurrent": True,
562
+ "transition": {"duration": 800}
563
+ }
564
+ ]
565
+ },
566
+ {
567
+ "label": "Pause",
568
+ "method": "animate",
569
+ "args": [
570
+ [None],
571
+ {
572
+ "frame": {"duration": 0, "redraw": False},
573
+ "mode": "immediate",
574
+ "transition": {"duration": 0}
575
+ }
576
+ ]
577
+ }
578
+ ]
579
+ }],
580
+ sliders=[{
581
+ "currentvalue": {"prefix": "Step: "},
582
+ "pad": {"t": 20},
583
+ "len": 0.9,
584
+ "x": 0.1,
585
+ "xanchor": "left",
586
+ "y": 0.02,
587
+ "yanchor": "bottom",
588
+ "steps": [
589
+ {
590
+ "args": [
591
+ [k],
592
+ {
593
+ "frame": {"duration": 1000, "redraw": True},
594
+ "transition": {"duration": 500},
595
+ "mode": "immediate"
596
+ }
597
+ ],
598
+ "label": str(k),
599
+ "method": "animate"
600
+ }
601
+ for k in range(len(df))
602
+ ]
603
+ }]
604
+ )
605
+
606
+ # Create complete HTML content
607
+ html_content = f"""
608
+ <!DOCTYPE html>
609
+ <html>
610
+ <head>
611
+ <meta charset="utf-8">
612
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
613
+ <title>Route Map</title>
614
+ <style>
615
+ body {{
616
+ margin: 0;
617
+ padding: 0;
618
+ width: 100vw;
619
+ height: 100vh;
620
+ overflow: hidden;
621
+ }}
622
+ #map-container {{
623
+ width: 100%;
624
+ height: 100%;
625
+ }}
626
+ </style>
627
+ </head>
628
+ <body>
629
+ <div id="map-container">
630
+ {fig.to_html(include_plotlyjs=True, full_html=False)}
631
+ </div>
632
+ <script>
633
+ window.onload = function() {{
634
+ setTimeout(function() {{
635
+ window.dispatchEvent(new Event('resize'));
636
+ }}, 1000);
637
+ }};
638
+ </script>
639
+ </body>
640
+ </html>
641
+ """
642
+
643
+ # Save the HTML to a file
644
+ file_path = 'temp/route_map.html'
645
+ with open(file_path, 'w', encoding='utf-8') as f:
646
+ f.write(html_content)
647
+
648
+ # Return the file
649
+ return FileResponse(file_path, media_type='text/html')
650
 
651
  except nx.NetworkXNoPath:
652
  raise HTTPException(status_code=404, detail="No route found")
653
 
 
 
654
  except Exception as e:
655
  raise HTTPException(status_code=400, detail=str(e))
656
+
657
  @app.get("/api/stores/locations", response_class=HTMLResponse, responses={400: {"model": ErrorResponse}})
658
  async def get_all_store_locations(
659
  lat: float = Query(..., description="Latitude of center point"),
660
  lon: float = Query(..., description="Longitude of center point"),
661
+ radius: float = Query(10, description="Search radius in kilometers")
662
  ):
663
+ """
664
+ Get a map showing all stores in the given radius with colors based on distance
665
+ """
666
  try:
 
 
 
667
  # Get nearby stores
668
  nearby_stores = store_locator.find_nearby_stores(lat, lon, radius)
669
 
 
 
 
 
670
  # Create base map centered on user location
671
  m = folium.Map(
672
  location=[lat, lon],
 
691
  else:
692
  color = 'blue' # Further away
693
 
694
+ # Create detailed popup content with mobile-friendly styling
695
  popup_content = f"""
696
  <div style='width: 200px; font-size: 14px;'>
697
  <h4 style='color: {color}; margin: 0 0 8px 0;'>{store['store_name']}</h4>
698
+ <b>Address:</b> {store['address']}<br>
699
  <b>Distance:</b> {store['distance']} km<br>
700
  <b>Est. Delivery:</b> {store['estimated_delivery_time']} mins<br>
701
+ <b>Contact:</b> {store['contact']}<br>
702
  <b>Categories:</b> {store['product_categories']}<br>
703
+ <button onclick="window.location.href='/api/stores/route?user_lat={lat}&user_lon={lon}&store_lat={store['location']['lat']}&store_lon={store['location']['lon']}'"
704
  style='margin-top: 8px; padding: 8px; width: 100%; background-color: #007bff; color: white; border: none; border-radius: 4px;'>
705
  Get Route
706
  </button>
 
715
  tooltip=f"{store['store_name']} ({store['distance']} km)"
716
  ).add_to(m)
717
 
718
+ # Add circle to show distance
719
+ folium.Circle(
720
+ location=[store['location']['lat'], store['location']['lon']],
721
+ radius=store['distance'] * 100,
722
+ color=color,
723
+ fill=True,
724
+ opacity=0.1
725
+ ).add_to(m)
 
726
 
727
+ # Add distance circles from user location
728
+ for radius_circle, color in [(2000, 'red'), (5000, 'orange'), (radius * 1000, 'blue')]:
729
  folium.Circle(
730
  location=[lat, lon],
731
+ radius=radius_circle,
732
  color=color,
733
  fill=False,
734
+ weight=1,
735
+ dash_array='5, 5'
736
  ).add_to(m)
737
 
738
  # Create mobile-friendly HTML content
 
807
  with open(file_path, 'w', encoding='utf-8') as f:
808
  f.write(html_content)
809
 
810
+ return FileResponse(file_path, media_type='text/html')
 
811
 
812
  except Exception as e:
813
  raise HTTPException(status_code=400, detail=str(e))
814
 
815
+ # Add swagger UI customization
816
+ @app.on_event("startup")
817
+ async def startup_event():
818
+ app.title = "Store Locator API"
819
+ app.description = "API for locating nearby stores and generating routes"
820
+ app.version = "1.0.0"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
821
 
822
+ # Entry point for running the application
823
  if __name__ == "__main__":
824
  import uvicorn
825
  uvicorn.run(app, host="0.0.0.0", port=8000)
dapp.py ADDED
@@ -0,0 +1,889 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, Query, HTTPException, Response
2
+ from fastapi.responses import JSONResponse, HTMLResponse, FileResponse
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ import pandas as pd
5
+ import numpy as np
6
+ from geopy.distance import geodesic
7
+ import folium
8
+ from folium import plugins
9
+ import osmnx as ox
10
+ import networkx as nx
11
+ from datetime import datetime
12
+ import json
13
+ import matplotlib.pyplot as plt
14
+ import plotly.express as px
15
+ import os
16
+ import time
17
+ from functools import lru_cache
18
+ from rtree import index
19
+ import gc
20
+ import shutil
21
+ from typing import Optional, List, Dict, Any, Union
22
+ from pydantic import BaseModel, Field
23
+
24
+ app = FastAPI(title="falcao-maps API", description="Store locator and route planning API")
25
+
26
+ # Add CORS middleware
27
+ app.add_middleware(
28
+ CORSMiddleware,
29
+ allow_origins=["*"],
30
+ allow_credentials=True,
31
+ allow_methods=["*"],
32
+ allow_headers=["*"],
33
+ )
34
+
35
+ # Create temp directory for files
36
+ os.makedirs('temp', exist_ok=True)
37
+
38
+ # Custom JSON encoder for NumPy types
39
+ class NumpyEncoder(json.JSONEncoder):
40
+ def default(self, obj):
41
+ if isinstance(obj, np.integer):
42
+ return int(obj)
43
+ elif isinstance(obj, np.floating):
44
+ return float(obj)
45
+ elif isinstance(obj, np.ndarray):
46
+ return obj.tolist()
47
+ return super(NumpyEncoder, self).default(obj)
48
+
49
+ # Load and prepare the store data
50
+ stores_df = pd.read_csv('dataset of 50 stores.csv')
51
+
52
+ # Define Pydantic models for API responses
53
+ class Location(BaseModel):
54
+ lat: float
55
+ lon: float
56
+
57
+ class Store(BaseModel):
58
+ store_name: str
59
+ address: str
60
+ contact: str
61
+ distance: float
62
+ estimated_delivery_time: int
63
+ product_categories: str
64
+ location: Location
65
+
66
+ class StoresResponse(BaseModel):
67
+ status: str
68
+ stores: List[Store]
69
+
70
+ class ErrorResponse(BaseModel):
71
+ status: str
72
+ message: str
73
+
74
+ class StoreLocator:
75
+ def __init__(self, stores_dataframe):
76
+ self.stores_df = stores_dataframe
77
+ self.network_graph = None
78
+ self.graph_cache = {} # Cache for network graphs
79
+ self.spatial_index = self._build_spatial_index()
80
+
81
+ @lru_cache(maxsize=50)
82
+ def initialize_graph(self, center_point, dist=30000): # Reduced distance for memory optimization
83
+ """Initialize road network graph with caching"""
84
+ cache_key = f"{center_point[0]}_{center_point[1]}"
85
+ if cache_key in self.graph_cache:
86
+ self.network_graph = self.graph_cache[cache_key]
87
+ return True
88
+ try:
89
+ # Use simplify=True and increased tolerance for lower memory usage
90
+ self.network_graph = ox.graph_from_point(
91
+ center_point,
92
+ dist=dist,
93
+ network_type="drive",
94
+ simplify=True,
95
+ retain_all=False
96
+ )
97
+ self.network_graph = ox.add_edge_speeds(self.network_graph)
98
+ self.network_graph = ox.add_edge_travel_times(self.network_graph)
99
+
100
+ # Store in cache
101
+ self.graph_cache[cache_key] = self.network_graph
102
+
103
+ # Force garbage collection
104
+ gc.collect()
105
+
106
+ return True
107
+ except Exception as e:
108
+ print(f"Error initializing graph: {str(e)}")
109
+ return False
110
+
111
+ def _build_spatial_index(self):
112
+ idx = index.Index()
113
+ for i, row in self.stores_df.iterrows():
114
+ idx.insert(i, (row['Latitude'], row['Longitude'],
115
+ row['Latitude'], row['Longitude']))
116
+ return idx
117
+
118
+ def calculate_distance(self, lat1, lon1, lat2, lon2):
119
+ """Calculate direct distance between two points"""
120
+ return geodesic((lat1, lon1), (lat2, lon2)).kilometers
121
+
122
+ def estimate_delivery_time(self, distance, current_time=None):
123
+ """Estimate delivery time based on distance and current time"""
124
+ if current_time is None:
125
+ current_time = datetime.now()
126
+
127
+ # Base time: 5 mins base + 2 mins per km
128
+ base_minutes = 5 + (distance * 2)
129
+
130
+ # Apply traffic multiplier based on time of day
131
+ hour = current_time.hour
132
+ if hour in [8, 9, 10, 17, 18, 19]: # Peak hours
133
+ multiplier = 1.5
134
+ elif hour in [23, 0, 1, 2, 3, 4]: # Off-peak hours
135
+ multiplier = 0.8
136
+ else: # Normal hours
137
+ multiplier = 1.0
138
+
139
+ return round(base_minutes * multiplier)
140
+
141
+ def find_nearby_stores(self, lat, lon, radius=5):
142
+ """Find stores within radius using spatial index"""
143
+ nearby_stores = []
144
+ bbox = (lat - radius/111.0, lon - radius/111.0,
145
+ lat + radius/111.0, lon + radius/111.0)
146
+
147
+ for store_id in self.spatial_index.intersection(bbox):
148
+ store = self.stores_df.iloc[store_id]
149
+ distance = self.calculate_distance(lat, lon,
150
+ store['Latitude'],
151
+ store['Longitude'])
152
+ if distance <= radius:
153
+ delivery_time = self.estimate_delivery_time(distance)
154
+ nearby_stores.append({
155
+ 'store_name': store['Store Name'],
156
+ 'address': store['Address'],
157
+ 'contact': str(store['Contact Number']), # Convert to string to avoid int64 issues
158
+ 'distance': round(distance, 2),
159
+ 'estimated_delivery_time': int(delivery_time), # Ensure integer type
160
+ 'product_categories': store['Product Categories'],
161
+ 'location': {
162
+ 'lat': float(store['Latitude']), # Ensure float type
163
+ 'lon': float(store['Longitude']) # Ensure float type
164
+ }
165
+ })
166
+
167
+ return sorted(nearby_stores, key=lambda x: x['distance'])
168
+
169
+ def create_store_map(self, center_lat, center_lon, radius=5):
170
+ """Create an interactive map with store locations - optimized for memory"""
171
+ # Create base map
172
+ m = folium.Map(
173
+ location=[center_lat, center_lon],
174
+ zoom_start=13,
175
+ tiles="cartodbpositron"
176
+ )
177
+
178
+ # Create marker cluster for better performance with many markers
179
+ marker_cluster = plugins.MarkerCluster().add_to(m)
180
+
181
+ # Add stores to map
182
+ nearby_stores = self.find_nearby_stores(center_lat, center_lon, radius)
183
+
184
+ # Limit the number of stores to reduce memory usage
185
+ max_stores = min(len(nearby_stores), 50) # Cap at 50 stores
186
+
187
+ for store in nearby_stores[:max_stores]:
188
+ # Prepare popup content
189
+ popup_content = f"""
190
+ <div style='width: 200px'>
191
+ <b>{store['store_name']}</b><br>
192
+ Address: {store['address']}<br>
193
+ Distance: {store['distance']} km<br>
194
+ Est. Delivery: {store['estimated_delivery_time']} mins<br>
195
+ Categories: {store['product_categories']}
196
+ </div>
197
+ """
198
+
199
+ # Add store marker
200
+ folium.Marker(
201
+ location=[store['location']['lat'], store['location']['lon']],
202
+ popup=folium.Popup(popup_content, max_width=300),
203
+ icon=folium.Icon(color='red', icon='info-sign')
204
+ ).add_to(marker_cluster)
205
+
206
+ # Add line to show distance from center (only for closer stores)
207
+ if store['distance'] <= nearby_stores[min(9, len(nearby_stores)-1)]['distance']:
208
+ folium.PolyLine(
209
+ locations=[[center_lat, center_lon],
210
+ [store['location']['lat'], store['location']['lon']]],
211
+ weight=2,
212
+ color='blue',
213
+ opacity=0.3
214
+ ).add_to(m)
215
+
216
+ # Add current location marker
217
+ folium.Marker(
218
+ location=[center_lat, center_lon],
219
+ popup='Your Location',
220
+ icon=folium.Icon(color='green', icon='home')
221
+ ).add_to(m)
222
+
223
+ # Add layer control only
224
+ folium.LayerControl().add_to(m)
225
+
226
+ return m
227
+
228
+ # Initialize store locator
229
+ store_locator = StoreLocator(stores_df)
230
+
231
+ # Helper functions for cleaning temporary files
232
+ def cleanup_temp_files():
233
+ temp_dir = 'temp'
234
+ if os.path.exists(temp_dir):
235
+ for file in os.listdir(temp_dir):
236
+ file_path = os.path.join(temp_dir, file)
237
+ try:
238
+ if os.path.isfile(file_path) and file.endswith('.html'):
239
+ # Delete files older than 1 hour
240
+ if os.path.getmtime(file_path) < time.time() - 3600:
241
+ os.remove(file_path)
242
+ except Exception as e:
243
+ print(f"Error cleaning up temp files: {e}")
244
+
245
+ # Register cleanup on startup and shutdown
246
+ @app.on_event("startup")
247
+ async def startup_event():
248
+ cleanup_temp_files()
249
+
250
+ @app.on_event("shutdown")
251
+ async def shutdown_event():
252
+ # Clean up all temporary files on shutdown
253
+ try:
254
+ shutil.rmtree('temp')
255
+ except Exception as e:
256
+ print(f"Error cleaning up temp directory: {e}")
257
+
258
+ # Routes
259
+ @app.get("/", response_class=HTMLResponse)
260
+ async def home():
261
+ """API Documentation Homepage"""
262
+ html_content = f"""
263
+ <!DOCTYPE html>
264
+ <html lang="en">
265
+ <head>
266
+ <meta charset="UTF-8">
267
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
268
+ <title>falcao-maps API Documentation</title>
269
+ <style>
270
+ body {{
271
+ font-family: Arial, sans-serif;
272
+ margin: 20px;
273
+ }}
274
+ h1, h2 {{
275
+ color: #333;
276
+ }}
277
+ pre {{
278
+ background-color: #f4f4f4;
279
+ padding: 10px;
280
+ border: 1px solid #ddd;
281
+ border-radius: 5px;
282
+ }}
283
+ </style>
284
+ </head>
285
+ <body>
286
+ <h1>Welcome to falcao-maps</h1>
287
+ <p>Based on your uploaded dataset and deployed API, here are example API calls for your client:</p>
288
+
289
+ <h2>1. Find Nearby Stores (JSON Response)</h2>
290
+ <pre>
291
+ /api/stores/nearby?lat=18.9695&lon=72.8320&radius=1
292
+ </pre>
293
+ <p>Use this to get store details near Market Road area within 1km</p>
294
+
295
+ <h2>2. View Basic Store Map</h2>
296
+ <pre>
297
+ /api/stores/map?lat=18.9701&lon=72.8330&radius=0.5
298
+ </pre>
299
+ <p>Shows map centered at Main Street with 500m radius</p>
300
+
301
+ <h2>3. View All Store Locations with Color Coding</h2>
302
+ <pre>
303
+ /api/stores/locations?lat=18.9685&lon=72.8325&radius=2
304
+ </pre>
305
+ <p>Shows detailed map with color-coded stores within 2km</p>
306
+
307
+ <h2>4. Get Route Between Points</h2>
308
+ <p>Example routes:</p>
309
+ <pre>
310
+ # Route from Park Avenue to Hill Road stores (use simple visualization for memory optimization)
311
+ /api/stores/route?user_lat=18.9710&user_lon=72.8335&store_lat=18.9705&store_lon=72.8345&viz_type=simple
312
+
313
+ # Route from Main Street to Market Road stores
314
+ /api/stores/route?user_lat=18.9701&user_lon=72.8330&store_lat=18.9695&store_lon=72.8320&viz_type=simple
315
+ </pre>
316
+
317
+ <h2>Key Location Points in Dataset:</h2>
318
+ <ul>
319
+ <li>Main Street Area: 18.9701, 72.8330</li>
320
+ <li>Park Avenue: 18.9710, 72.8335</li>
321
+ <li>Market Road: 18.9695, 72.8320</li>
322
+ <li>Shopping Center: 18.9670, 72.8300</li>
323
+ <li>Commercial Street: 18.9690, 72.8340</li>
324
+ </ul>
325
+
326
+ <h2>API Documentation</h2>
327
+ <p>You can view the interactive API documentation at: <a href="/docs">/docs</a></p>
328
+ </body>
329
+ </html>
330
+ """
331
+ return html_content
332
+
333
+ @app.get("/api/stores/nearby", response_model=StoresResponse, responses={400: {"model": ErrorResponse}})
334
+ async def get_nearby_stores(
335
+ lat: float = Query(..., description="Latitude of user location"),
336
+ lon: float = Query(..., description="Longitude of user location"),
337
+ radius: float = Query(5.0, description="Search radius in kilometers")
338
+ ):
339
+ """Get nearby stores based on user location"""
340
+ try:
341
+ nearby_stores = store_locator.find_nearby_stores(lat, lon, radius)
342
+ return {"status": "success", "stores": nearby_stores}
343
+ except Exception as e:
344
+ raise HTTPException(status_code=400, detail=str(e))
345
+
346
+ @app.get("/api/stores/map", response_class=HTMLResponse, responses={400: {"model": ErrorResponse}})
347
+ async def get_stores_map(
348
+ lat: float = Query(..., description="Latitude of center point"),
349
+ lon: float = Query(..., description="Longitude of center point"),
350
+ radius: float = Query(5.0, description="Search radius in kilometers")
351
+ ):
352
+ """Get HTML map with store locations"""
353
+ try:
354
+ # Clean up temp files before creating new ones
355
+ cleanup_temp_files()
356
+
357
+ store_map = store_locator.create_store_map(lat, lon, radius)
358
+
359
+ # Create complete HTML content
360
+ html_content = f"""
361
+ <!DOCTYPE html>
362
+ <html>
363
+ <head>
364
+ <meta charset="utf-8">
365
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
366
+ <title>Stores Map</title>
367
+ <style>
368
+ body {{
369
+ margin: 0;
370
+ padding: 0;
371
+ width: 100vw;
372
+ height: 100vh;
373
+ overflow: hidden;
374
+ }}
375
+ #map {{
376
+ width: 100%;
377
+ height: 100%;
378
+ }}
379
+ </style>
380
+ </head>
381
+ <body>
382
+ {store_map.get_root().render()}
383
+ <script>
384
+ window.onload = function() {{
385
+ setTimeout(function() {{
386
+ window.dispatchEvent(new Event('resize'));
387
+ }}, 1000);
388
+ }};
389
+ </script>
390
+ </body>
391
+ </html>
392
+ """
393
+
394
+ # Save the HTML to a file
395
+ file_path = 'temp/stores_map.html'
396
+ with open(file_path, 'w', encoding='utf-8') as f:
397
+ f.write(html_content)
398
+
399
+ # Return the file as HTML response
400
+ return html_content
401
+
402
+ except Exception as e:
403
+ raise HTTPException(status_code=400, detail=str(e))
404
+
405
+ @app.get("/api/stores/route", response_class=HTMLResponse, responses={400: {"model": ErrorResponse}, 404: {"model": ErrorResponse}})
406
+ async def get_store_route(
407
+ user_lat: float = Query(..., description="User location latitude"),
408
+ user_lon: float = Query(..., description="User location longitude"),
409
+ store_lat: float = Query(..., description="Store location latitude"),
410
+ store_lon: float = Query(..., description="Store location longitude"),
411
+ viz_type: str = Query("simple", description="Visualization type (simple or advanced)")
412
+ ):
413
+ """Get route between user and store locations with visualization"""
414
+ try:
415
+ # Clean up temp files before creating new ones
416
+ cleanup_temp_files()
417
+
418
+ # Initialize graph if not already initialized
419
+ # Use a smaller distance to reduce memory usage
420
+ if store_locator.network_graph is None:
421
+ success = store_locator.initialize_graph((user_lat, user_lon), dist=10000)
422
+ if not success:
423
+ raise HTTPException(status_code=400, detail="Unable to initialize graph, try a different location")
424
+
425
+ # Get nearest nodes
426
+ start_node = ox.distance.nearest_nodes(
427
+ store_locator.network_graph, user_lon, user_lat)
428
+ end_node = ox.distance.nearest_nodes(
429
+ store_locator.network_graph, store_lon, store_lat)
430
+
431
+ try:
432
+ # Calculate path using the travel_time weight
433
+ path_time = nx.shortest_path(
434
+ store_locator.network_graph,
435
+ start_node,
436
+ end_node,
437
+ weight='travel_time'
438
+ )
439
+
440
+ if viz_type == "simple":
441
+ # Create a simple folium map for low-resource environments
442
+ m = folium.Map(
443
+ location=[(user_lat + store_lat) / 2, (user_lon + store_lon) / 2],
444
+ zoom_start=15,
445
+ tiles="cartodbpositron"
446
+ )
447
+
448
+ # Add markers for start and end points
449
+ folium.Marker(
450
+ [user_lat, user_lon],
451
+ popup='Your Location',
452
+ icon=folium.Icon(color='green', icon='home')
453
+ ).add_to(m)
454
+
455
+ folium.Marker(
456
+ [store_lat, store_lon],
457
+ popup='Store Location',
458
+ icon=folium.Icon(color='red', icon='info-sign')
459
+ ).add_to(m)
460
+
461
+ # Extract coordinates from the path
462
+ path_coords = []
463
+ for node in path_time:
464
+ x = store_locator.network_graph.nodes[node]['x']
465
+ y = store_locator.network_graph.nodes[node]['y']
466
+ path_coords.append([y, x]) # Note the y, x order for folium
467
+
468
+ # Add the route line
469
+ folium.PolyLine(
470
+ locations=path_coords,
471
+ weight=5,
472
+ color='blue',
473
+ opacity=0.7
474
+ ).add_to(m)
475
+
476
+ # Add distance and time estimate
477
+ total_distance = 0
478
+ total_time = 0
479
+
480
+ for i in range(len(path_time) - 1):
481
+ a, b = path_time[i], path_time[i + 1]
482
+ total_distance += store_locator.network_graph.edges[(a, b, 0)]['length']
483
+ total_time += store_locator.network_graph.edges[(a, b, 0)]['travel_time']
484
+
485
+ # Convert to km and minutes
486
+ total_distance_km = round(total_distance / 1000, 2)
487
+ total_time_min = round(total_time / 60, 1)
488
+
489
+ # Add info box
490
+ html_content = f"""
491
+ <div style="position: fixed; top: 10px; left: 50px; z-index: 9999;
492
+ background-color: white; padding: 10px; border-radius: 5px;
493
+ box-shadow: 0 0 10px rgba(0,0,0,0.3);">
494
+ <h4 style="margin: 0 0 5px 0;">Route Information</h4>
495
+ <p><b>Distance:</b> {total_distance_km} km<br>
496
+ <b>Est. Time:</b> {total_time_min} minutes</p>
497
+ </div>
498
+ """
499
+
500
+ m.get_root().html.add_child(folium.Element(html_content))
501
+
502
+ # Add layer control
503
+ folium.LayerControl().add_to(m)
504
+
505
+ # Create complete HTML content
506
+ html_content = f"""
507
+ <!DOCTYPE html>
508
+ <html>
509
+ <head>
510
+ <meta charset="utf-8">
511
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
512
+ <title>Simple Route Map</title>
513
+ <style>
514
+ body {{
515
+ margin: 0;
516
+ padding: 0;
517
+ width: 100vw;
518
+ height: 100vh;
519
+ overflow: hidden;
520
+ }}
521
+ #map {{
522
+ width: 100%;
523
+ height: 100%;
524
+ }}
525
+ </style>
526
+ </head>
527
+ <body>
528
+ {m.get_root().render()}
529
+ <script>
530
+ window.onload = function() {{
531
+ setTimeout(function() {{
532
+ window.dispatchEvent(new Event('resize'));
533
+ }}, 1000);
534
+ }};
535
+ </script>
536
+ </body>
537
+ </html>
538
+ """
539
+
540
+ # Save the HTML to a file
541
+ file_path = 'temp/simple_route_map.html'
542
+ with open(file_path, 'w', encoding='utf-8') as f:
543
+ f.write(html_content)
544
+
545
+ # Return the HTML content
546
+ return html_content
547
+
548
+ else:
549
+ # WARNING: Advanced visualization - may cause memory issues on limited resources
550
+
551
+ # Limit the path nodes to reduce memory usage
552
+ # Only include every Nth node
553
+ step = max(1, len(path_time) // 30) # Maximum 30 points
554
+ simplified_path = path_time[::step]
555
+ if path_time[-1] not in simplified_path:
556
+ simplified_path.append(path_time[-1])
557
+
558
+ # Create animation data (simplified)
559
+ lst_start, lst_end = [], []
560
+ start_x, start_y = [], []
561
+ end_x, end_y = [], []
562
+ lst_length, lst_time = [], []
563
+
564
+ for a, b in zip(simplified_path[:-1], simplified_path[1:]):
565
+ lst_start.append(a)
566
+ lst_end.append(b)
567
+
568
+ # Calculate accumulated length and time between simplified points
569
+ segment_length = 0
570
+ segment_time = 0
571
+ path_segment = nx.shortest_path(
572
+ store_locator.network_graph, a, b, weight='travel_time')
573
+
574
+ for i in range(len(path_segment) - 1):
575
+ u, v = path_segment[i], path_segment[i + 1]
576
+ segment_length += store_locator.network_graph.edges[(u, v, 0)]['length']
577
+ segment_time += store_locator.network_graph.edges[(u, v, 0)]['travel_time']
578
+
579
+ lst_length.append(round(segment_length))
580
+ lst_time.append(round(segment_time))
581
+ start_x.append(store_locator.network_graph.nodes[a]['x'])
582
+ start_y.append(store_locator.network_graph.nodes[a]['y'])
583
+ end_x.append(store_locator.network_graph.nodes[b]['x'])
584
+ end_y.append(store_locator.network_graph.nodes[b]['y'])
585
+
586
+ df = pd.DataFrame(
587
+ list(zip(lst_start, lst_end, start_x, start_y, end_x, end_y,
588
+ lst_length, lst_time)),
589
+ columns=["start", "end", "start_x", "start_y",
590
+ "end_x", "end_y", "length", "travel_time"]
591
+ ).reset_index().rename(columns={"index": "id"})
592
+
593
+ # Create animation using plotly (reduced complexity)
594
+ df_start = df[df["start"] == lst_start[0]]
595
+ df_end = df[df["end"] == lst_end[-1]]
596
+
597
+ fig = px.scatter_mapbox(
598
+ data_frame=df,
599
+ lon="start_x",
600
+ lat="start_y",
601
+ zoom=15,
602
+ width=800, # Reduced size
603
+ height=600, # Reduced size
604
+ animation_frame="id",
605
+ mapbox_style="carto-positron"
606
+ )
607
+
608
+ # Basic visualization elements only
609
+ fig.data[0].marker = {"size": 12}
610
+
611
+ # Add start point
612
+ fig.add_trace(
613
+ px.scatter_mapbox(
614
+ data_frame=df_start,
615
+ lon="start_x",
616
+ lat="start_y"
617
+ ).data[0]
618
+ )
619
+ fig.data[1].marker = {"size": 15, "color": "red"}
620
+
621
+ # Add end point
622
+ fig.add_trace(
623
+ px.scatter_mapbox(
624
+ data_frame=df_end,
625
+ lon="start_x",
626
+ lat="start_y"
627
+ ).data[0]
628
+ )
629
+ fig.data[2].marker = {"size": 15, "color": "green"}
630
+
631
+ # Add route
632
+ fig.add_trace(
633
+ px.line_mapbox(
634
+ data_frame=df,
635
+ lon="start_x",
636
+ lat="start_y"
637
+ ).data[0]
638
+ )
639
+
640
+ # Simplified layout with fewer options to reduce complexity
641
+ fig.update_layout(
642
+ showlegend=False,
643
+ margin={"r":0,"t":0,"l":0,"b":0},
644
+ autosize=True,
645
+ height=None
646
+ )
647
+
648
+ # Create complete HTML content
649
+ html_content = f"""
650
+ <!DOCTYPE html>
651
+ <html>
652
+ <head>
653
+ <meta charset="utf-8">
654
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
655
+ <title>Route Map</title>
656
+ <style>
657
+ body {{
658
+ margin: 0;
659
+ padding: 0;
660
+ width: 100vw;
661
+ height: 100vh;
662
+ overflow: hidden;
663
+ }}
664
+ #map-container {{
665
+ width: 100%;
666
+ height: 100%;
667
+ }}
668
+ </style>
669
+ </head>
670
+ <body>
671
+ <div id="map-container">
672
+ {fig.to_html(include_plotlyjs=True, full_html=False, config={'staticPlot': True})}
673
+ </div>
674
+ <script>
675
+ window.onload = function() {{
676
+ setTimeout(function() {{
677
+ window.dispatchEvent(new Event('resize'));
678
+ }}, 1000);
679
+ }};
680
+ </script>
681
+ </body>
682
+ </html>
683
+ """
684
+
685
+ # Save the HTML to a file
686
+ file_path = 'temp/route_map.html'
687
+ with open(file_path, 'w', encoding='utf-8') as f:
688
+ f.write(html_content)
689
+
690
+ # Return the HTML content
691
+ return html_content
692
+
693
+ except nx.NetworkXNoPath:
694
+ raise HTTPException(status_code=404, detail="No route found")
695
+
696
+ except HTTPException:
697
+ raise
698
+ except Exception as e:
699
+ raise HTTPException(status_code=400, detail=str(e))
700
+
701
+ @app.get("/api/stores/locations", response_class=HTMLResponse, responses={400: {"model": ErrorResponse}})
702
+ async def get_all_store_locations(
703
+ lat: float = Query(..., description="Latitude of center point"),
704
+ lon: float = Query(..., description="Longitude of center point"),
705
+ radius: float = Query(10.0, description="Search radius in kilometers")
706
+ ):
707
+ """Get a map showing all stores in the given radius with colors based on distance"""
708
+ try:
709
+ # Clean up temp files before creating new ones
710
+ cleanup_temp_files()
711
+
712
+ # Get nearby stores
713
+ nearby_stores = store_locator.find_nearby_stores(lat, lon, radius)
714
+
715
+ # Limit number of stores for memory optimization
716
+ max_stores = min(len(nearby_stores), 50)
717
+ nearby_stores = nearby_stores[:max_stores]
718
+
719
+ # Create base map centered on user location
720
+ m = folium.Map(
721
+ location=[lat, lon],
722
+ zoom_start=12,
723
+ tiles="cartodbpositron"
724
+ )
725
+
726
+ # Add user location marker
727
+ folium.Marker(
728
+ [lat, lon],
729
+ popup='Your Location',
730
+ icon=folium.Icon(color='green', icon='home')
731
+ ).add_to(m)
732
+
733
+ # Add markers for each store with color coding based on distance
734
+ for store in nearby_stores:
735
+ # Color code based on distance
736
+ if store['distance'] <= 2:
737
+ color = 'red' # Very close
738
+ elif store['distance'] <= 5:
739
+ color = 'orange' # Moderate distance
740
+ else:
741
+ color = 'blue' # Further away
742
+
743
+ # Create simplified popup content
744
+ popup_content = f"""
745
+ <div style='width: 200px; font-size: 14px;'>
746
+ <h4 style='color: {color}; margin: 0 0 8px 0;'>{store['store_name']}</h4>
747
+ <b>Distance:</b> {store['distance']} km<br>
748
+ <b>Est. Delivery:</b> {store['estimated_delivery_time']} mins<br>
749
+ <b>Categories:</b> {store['product_categories']}<br>
750
+ <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'"
751
+ style='margin-top: 8px; padding: 8px; width: 100%; background-color: #007bff; color: white; border: none; border-radius: 4px;'>
752
+ Get Route
753
+ </button>
754
+ </div>
755
+ """
756
+
757
+ # Add store marker
758
+ folium.Marker(
759
+ location=[store['location']['lat'], store['location']['lon']],
760
+ popup=folium.Popup(popup_content, max_width=300),
761
+ icon=folium.Icon(color=color, icon='info-sign'),
762
+ tooltip=f"{store['store_name']} ({store['distance']} km)"
763
+ ).add_to(m)
764
+
765
+ # Add circle to show distance - only for closer stores to reduce complexity
766
+ if store['distance'] <= 5:
767
+ folium.Circle(
768
+ location=[store['location']['lat'], store['location']['lon']],
769
+ radius=store['distance'] * 100,
770
+ color=color,
771
+ fill=True,
772
+ opacity=0.1
773
+ ).add_to(m)
774
+
775
+ # Add distance circles from user location - reduced to save memory
776
+ for circle_radius, color in [(2000, 'red'), (5000, 'orange')]:
777
+ folium.Circle(
778
+ location=[lat, lon],
779
+ radius=circle_radius,
780
+ color=color,
781
+ fill=False,
782
+ weight=1,dash_array='5, 5'
783
+ ).add_to(m)
784
+
785
+ # Create mobile-friendly HTML content
786
+ html_content = f"""
787
+ <!DOCTYPE html>
788
+ <html>
789
+ <head>
790
+ <meta charset="utf-8">
791
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
792
+ <title>Nearby Stores</title>
793
+ <style>
794
+ body {{
795
+ margin: 0;
796
+ padding: 0;
797
+ width: 100vw;
798
+ height: 100vh;
799
+ overflow: hidden;
800
+ }}
801
+ #map {{
802
+ width: 100%;
803
+ height: 100%;
804
+ }}
805
+ .legend {{
806
+ position: fixed;
807
+ bottom: 20px;
808
+ right: 20px;
809
+ background: white;
810
+ padding: 10px;
811
+ border-radius: 5px;
812
+ box-shadow: 0 1px 5px rgba(0,0,0,0.2);
813
+ font-size: 12px;
814
+ z-index: 1000;
815
+ }}
816
+ .info-box {{
817
+ position: fixed;
818
+ top: 20px;
819
+ left: 20px;
820
+ background: white;
821
+ padding: 10px;
822
+ border-radius: 5px;
823
+ box-shadow: 0 1px 5px rgba(0,0,0,0.2);
824
+ font-size: 12px;
825
+ z-index: 1000;
826
+ }}
827
+ </style>
828
+ </head>
829
+ <body>
830
+ {m.get_root().render()}
831
+ <div class="legend">
832
+ <b>Distance Zones</b><br>
833
+ <span style="color: red;">●</span> &lt; 2 km<br>
834
+ <span style="color: orange;">●</span> 2-5 km<br>
835
+ <span style="color: blue;">●</span> &gt; 5 km
836
+ </div>
837
+ <div class="info-box">
838
+ <b>Search Radius:</b> {radius} km<br>
839
+ <b>Stores Found:</b> {len(nearby_stores)}
840
+ </div>
841
+ <script>
842
+ window.onload = function() {{
843
+ setTimeout(function() {{
844
+ window.dispatchEvent(new Event('resize'));
845
+ }}, 1000);
846
+ }};
847
+ </script>
848
+ </body>
849
+ </html>
850
+ """
851
+
852
+ # Save and return the file
853
+ file_path = 'temp/locations_map.html'
854
+ with open(file_path, 'w', encoding='utf-8') as f:
855
+ f.write(html_content)
856
+
857
+ # Return the HTML content
858
+ return html_content
859
+
860
+ except Exception as e:
861
+ raise HTTPException(status_code=400, detail=str(e))
862
+
863
+ # Add endpoint to serve static files directly
864
+ @app.get("/temp/{file_path:path}", response_class=FileResponse)
865
+ async def get_temp_file(file_path: str):
866
+ """Serve temporary files like HTML maps"""
867
+ full_path = os.path.join("temp", file_path)
868
+ if not os.path.exists(full_path):
869
+ raise HTTPException(status_code=404, detail="File not found")
870
+ return FileResponse(full_path)
871
+
872
+ # Add memory monitoring and management middleware
873
+ @app.middleware("http")
874
+ async def add_memory_management(request: Request, call_next):
875
+ # Cleanup before processing request
876
+ cleanup_temp_files()
877
+
878
+ # Process the request
879
+ response = await call_next(request)
880
+
881
+ # Cleanup after processing request
882
+ gc.collect()
883
+
884
+ return response
885
+
886
+ # For running the application directly (development mode)
887
+ if __name__ == "__main__":
888
+ import uvicorn
889
+ uvicorn.run(app, host="0.0.0.0", port=8000)