forever-sheikh commited on
Commit
e3fd865
Β·
verified Β·
1 Parent(s): 6e2e86b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +222 -0
app.py ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import googlemaps
3
+ import osmnx as ox
4
+ import geopandas as gpd
5
+ import pandas as pd
6
+ import requests
7
+ import zipfile
8
+ import os
9
+ import glob
10
+ import shutil
11
+ from shapely.geometry import Point, Polygon
12
+
13
+ # ==========================================
14
+ # AUTHENTICATION
15
+ # ==========================================
16
+ try:
17
+ API_KEY = os.environ.get("GOOGLE_API_KEY")
18
+ except:
19
+ API_KEY = None
20
+
21
+ # ==========================================
22
+ # HELPER FUNCTIONS
23
+ # ==========================================
24
+ def load_geodata_to_polygon(file_obj):
25
+ extract_path = "temp_extract"
26
+ if os.path.exists(extract_path):
27
+ shutil.rmtree(extract_path)
28
+ os.makedirs(extract_path)
29
+
30
+ target_kml = None
31
+ try:
32
+ if file_obj.name.lower().endswith('.kmz'):
33
+ with zipfile.ZipFile(file_obj.name, 'r') as zip_ref:
34
+ zip_ref.extractall(extract_path)
35
+ kml_files = glob.glob(extract_path + "/**/*.kml", recursive=True)
36
+ if kml_files:
37
+ target_kml = kml_files[0]
38
+ elif file_obj.name.lower().endswith('.kml'):
39
+ target_kml = file_obj.name
40
+
41
+ if target_kml:
42
+ return gpd.read_file(target_kml).union_all()
43
+ except:
44
+ return None
45
+ return None
46
+
47
+ def get_roof_area(lat, lng, api_key):
48
+ base_url = "https://solar.googleapis.com/v1/buildingInsights:findClosest"
49
+ params = {
50
+ "location.latitude": lat,
51
+ "location.longitude": lng,
52
+ "requiredQuality": "HIGH",
53
+ "key": api_key
54
+ }
55
+ try:
56
+ resp = requests.get(base_url, params=params)
57
+ data = resp.json()
58
+ if 'error' in data: return None
59
+ return data.get('solarPotential', {}).get('wholeRoofStats', {}).get('areaMeters2', None)
60
+ except:
61
+ return None
62
+
63
+ def get_osm_physics(lat, lng):
64
+ try:
65
+ tags = {'building': True}
66
+ gdf = ox.features.features_from_point((lat, lng), tags, dist=60)
67
+ if not gdf.empty:
68
+ gdf_proj = gdf.to_crs(epsg=3857)
69
+ gdf_proj['area_m2'] = gdf_proj.geometry.area
70
+ best = gdf_proj.sort_values(by='area_m2', ascending=False).iloc[0]
71
+
72
+ floors = None
73
+ if 'building:levels' in best and pd.notna(best['building:levels']):
74
+ try: floors = int(float(str(best['building:levels']).split(';')[0]))
75
+ except: pass
76
+
77
+ height = None
78
+ if 'height' in best and pd.notna(best['height']):
79
+ try: height = float(str(best['height']).replace('m','').strip())
80
+ except: pass
81
+ return height, floors
82
+ except:
83
+ pass
84
+ return None, None
85
+
86
+ # DATA CONSTANTS
87
+ BRAND_FLOORS = {
88
+ "Macy's": 2, "JCPenney": 2, "Nordstrom": 2, "Sears": 2, "IKEA": 2,
89
+ "Target": 1, "Walmart": 1, "Costco": 1, "Home Depot": 1, "Lowe's": 1,
90
+ "Barnes & Noble": 1, "Dick's Sporting Goods": 1, "Kohl's": 1
91
+ }
92
+
93
+ BRAND_AVG_AREA = {
94
+ "IKEA": 28000, "Walmart": 15000, "Costco": 14000, "Sam's Club": 13000,
95
+ "Meijer": 18000, "Target": 12000, "Home Depot": 10000, "Lowe's": 10000,
96
+ "Kroger": 6000, "Safeway": 5000, "Whole Foods": 4000, "Macy's": 16000,
97
+ "JCPenney": 10000, "Sears": 12000, "Kohl's": 8000, "Dick's Sporting Goods": 4500,
98
+ "T.J. Maxx": 2800, "Marshalls": 2800, "Ross Dress for Less": 2800,
99
+ "Old Navy": 1400, "Barnes & Noble": 2500, "Best Buy": 3500, "Staples": 2000,
100
+ "Office Depot": 2000, "PetSmart": 1800, "Petco": 1400, "Trader Joe's": 1200,
101
+ "Aldi": 1500, "Lidl": 1500, "Ace Hardware": 800, "DSW Designer Shoe Warehouse": 2000
102
+ }
103
+
104
+ SEARCH_LIST = [
105
+ "Walmart", "Target", "Kmart", "Sears", "Kohl's", "Macy's", "JCPenney",
106
+ "T.J. Maxx", "Marshalls", "HomeGoods", "HomeSense", "Ross Dress for Less", "Burlington",
107
+ "Dick's Sporting Goods", "Albertsons", "Safeway", "Home Depot", "Lowe's",
108
+ "Best Buy", "IKEA", "Bob's Discount Furniture", "Raymour & Flanigan",
109
+ "Barnes & Noble", "Office Depot", "OfficeMax", "Staples",
110
+ "PetSmart", "Petco", "Kroger", "Meijer", "Costco", "BJ's Wholesale Club",
111
+ "Sam's Club", "Whole Foods", "ShopRite", "Stop & Shop", "Trader Joe's",
112
+ "Lidl", "Aldi", "DSW Designer Shoe Warehouse", "Old Navy", "Ace Hardware"
113
+ ]
114
+
115
+ # ==========================================
116
+ # MAIN LOGIC WITH GENERATOR (YIELD)
117
+ # ==========================================
118
+ def process_data(file_obj):
119
+ # This allows real-time updates to the UI
120
+ if not API_KEY:
121
+ yield "❌ API Key not found! Set GOOGLE_API_KEY in Secrets.", None
122
+ return
123
+
124
+ if file_obj is None:
125
+ yield "❌ Please upload a file.", None
126
+ return
127
+
128
+ yield "πŸ“‚ Loading Polygon...", None
129
+ polygon = load_geodata_to_polygon(file_obj)
130
+
131
+ if not polygon:
132
+ yield "❌ Failed to read KML/KMZ file.", None
133
+ return
134
+
135
+ gmaps = googlemaps.Client(key=API_KEY)
136
+ results = []
137
+ seen_ids = set()
138
+ total_brands = len(SEARCH_LIST)
139
+
140
+ # LOOP THROUGH BRANDS
141
+ for i, brand in enumerate(SEARCH_LIST):
142
+ # Update the UI status message
143
+ yield f"πŸ” Scanning Brand {i+1}/{total_brands}: {brand}...", None
144
+
145
+ try:
146
+ places = gmaps.places_nearby(
147
+ location=(polygon.centroid.y, polygon.centroid.x),
148
+ radius=50000,
149
+ keyword=brand
150
+ )
151
+
152
+ for p in places.get('results', []):
153
+ pid = p.get('place_id')
154
+ if pid in seen_ids: continue
155
+
156
+ name = p.get('name')
157
+ if any(x in name for x in ["ATM", "Redbox", "Kiosk", "Coinme", "Gas", "Fuel"]): continue
158
+
159
+ lat = p['geometry']['location']['lat']
160
+ lng = p['geometry']['location']['lng']
161
+
162
+ if not polygon.contains(Point(lng, lat)): continue
163
+ seen_ids.add(pid)
164
+
165
+ # Get Data
166
+ roof_area = get_roof_area(lat, lng, API_KEY)
167
+ height, floors = get_osm_physics(lat, lng)
168
+
169
+ # Logic
170
+ source_note = "SolarAPI"
171
+ if roof_area is None:
172
+ roof_area = BRAND_AVG_AREA.get(brand, 2500.0)
173
+ source_note = "Brand_Avg (Missing)"
174
+ else:
175
+ if roof_area > 30000 and brand not in ["IKEA", "Costco", "Meijer", "Sam's Club", "Walmart"]:
176
+ roof_area = BRAND_AVG_AREA.get(brand, 2500.0)
177
+ source_note = "Brand_Avg (Mall detected)"
178
+ elif roof_area < 500 and brand not in ["Ace Hardware", "Trader Joe's", "GameStop"]:
179
+ roof_area = BRAND_AVG_AREA.get(brand, 2500.0)
180
+ source_note = "Brand_Avg (Too Small detected)"
181
+
182
+ if floors is None: floors = BRAND_FLOORS.get(brand, 1)
183
+ if height is None: height = floors * 6.0
184
+
185
+ results.append({
186
+ 'Name': name, 'Brand': brand, 'Latitude': lat, 'Longitude': lng,
187
+ 'Height_m': round(height, 2), 'Num_Floors': int(floors),
188
+ 'Area_sqm': round(roof_area, 2), 'Data_Source': source_note
189
+ })
190
+ except:
191
+ pass
192
+
193
+ # FINISHED
194
+ if not results:
195
+ yield "❌ No stores found in this area.", None
196
+ return
197
+
198
+ # Export
199
+ df = pd.DataFrame(results)
200
+ bad_keywords = ['Mobile', 'Salon', 'Floral', 'Bakery', 'Pharmacy', 'Optical', 'Geek Squad', 'Photo']
201
+ df = df[~df['Name'].str.contains('|'.join(bad_keywords), case=False, na=False)]
202
+
203
+ output_path = "Building_Inventory.csv"
204
+ df.to_csv(output_path, index=False)
205
+
206
+ yield f"βœ… Success! Found {len(df)} stores.", output_path
207
+
208
+ # ==========================================
209
+ # GRADIO INTERFACE
210
+ # ==========================================
211
+ iface = gr.Interface(
212
+ fn=process_data,
213
+ inputs=gr.File(label="Upload Polygon (.kmz or .kml)"),
214
+ outputs=[
215
+ gr.Textbox(label="Status Log"),
216
+ gr.File(label="Download CSV")
217
+ ],
218
+ title="πŸ™οΈ Commercial Building Inventory Generator",
219
+ description="Upload a KMZ file. The tool will scan for 42 Big Box brands, check Solar API for area, and OpenStreetMap for height/floors."
220
+ )
221
+
222
+ iface.launch()