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

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +267 -0
app.py ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ import time # Required for Google API Pagination
12
+ from shapely.geometry import Point, Polygon
13
+ from shapely.ops import transform
14
+
15
+ # ==========================================
16
+ # AUTHENTICATION
17
+ # ==========================================
18
+ try:
19
+ API_KEY = os.environ.get("GOOGLE_API_KEY")
20
+ except:
21
+ API_KEY = None
22
+
23
+ # ==========================================
24
+ # HELPER FUNCTIONS
25
+ # ==========================================
26
+ def load_geodata_to_polygon(file_obj):
27
+ extract_path = "temp_extract"
28
+ if os.path.exists(extract_path):
29
+ shutil.rmtree(extract_path)
30
+ os.makedirs(extract_path)
31
+
32
+ target_kml = None
33
+ try:
34
+ if file_obj.name.lower().endswith('.kmz'):
35
+ with zipfile.ZipFile(file_obj.name, 'r') as zip_ref:
36
+ zip_ref.extractall(extract_path)
37
+ kml_files = glob.glob(extract_path + "/**/*.kml", recursive=True)
38
+ if kml_files:
39
+ target_kml = kml_files[0]
40
+ elif file_obj.name.lower().endswith('.kml'):
41
+ target_kml = file_obj.name
42
+
43
+ if target_kml:
44
+ gdf = gpd.read_file(target_kml)
45
+
46
+ # --- FIX: FORCE 2D ---
47
+ # Flattens 3D coordinates so the "Strict Check" works perfectly.
48
+ def force_2d(geometry):
49
+ if geometry.has_z:
50
+ return transform(lambda x, y, z=None: (x, y), geometry)
51
+ return geometry
52
+
53
+ gdf.geometry = gdf.geometry.apply(force_2d)
54
+ # ---------------------
55
+
56
+ return gdf.union_all()
57
+ except:
58
+ return None
59
+ return None
60
+
61
+ def get_roof_area(lat, lng, api_key):
62
+ base_url = "https://solar.googleapis.com/v1/buildingInsights:findClosest"
63
+ params = {
64
+ "location.latitude": lat,
65
+ "location.longitude": lng,
66
+ "requiredQuality": "HIGH",
67
+ "key": api_key
68
+ }
69
+ try:
70
+ resp = requests.get(base_url, params=params)
71
+ data = resp.json()
72
+ if 'error' in data: return None
73
+ return data.get('solarPotential', {}).get('wholeRoofStats', {}).get('areaMeters2', None)
74
+ except:
75
+ return None
76
+
77
+ def get_osm_physics(lat, lng):
78
+ try:
79
+ tags = {'building': True}
80
+ gdf = ox.features.features_from_point((lat, lng), tags, dist=60)
81
+ if not gdf.empty:
82
+ gdf_proj = gdf.to_crs(epsg=3857)
83
+ gdf_proj['area_m2'] = gdf_proj.geometry.area
84
+ best = gdf_proj.sort_values(by='area_m2', ascending=False).iloc[0]
85
+
86
+ floors = None
87
+ if 'building:levels' in best and pd.notna(best['building:levels']):
88
+ try: floors = int(float(str(best['building:levels']).split(';')[0]))
89
+ except: pass
90
+
91
+ height = None
92
+ if 'height' in best and pd.notna(best['height']):
93
+ try: height = float(str(best['height']).replace('m','').strip())
94
+ except: pass
95
+ return height, floors
96
+ except:
97
+ pass
98
+ return None, None
99
+
100
+ # DATA CONSTANTS
101
+ BRAND_FLOORS = {
102
+ "Macy's": 2, "JCPenney": 2, "Nordstrom": 2, "Sears": 2, "IKEA": 2,
103
+ "Target": 1, "Walmart": 1, "Costco": 1, "Home Depot": 1, "Lowe's": 1,
104
+ "Barnes & Noble": 1, "Dick's Sporting Goods": 1, "Kohl's": 1
105
+ }
106
+
107
+ BRAND_AVG_AREA = {
108
+ "IKEA": 28000, "Walmart": 15000, "Costco": 14000, "Sam's Club": 13000,
109
+ "Meijer": 18000, "Target": 12000, "Home Depot": 10000, "Lowe's": 10000,
110
+ "Kroger": 6000, "Safeway": 5000, "Whole Foods": 4000, "Macy's": 16000,
111
+ "JCPenney": 10000, "Sears": 12000, "Kohl's": 8000, "Dick's Sporting Goods": 4500,
112
+ "T.J. Maxx": 2800, "Marshalls": 2800, "Ross Dress for Less": 2800,
113
+ "Old Navy": 1400, "Barnes & Noble": 2500, "Best Buy": 3500, "Staples": 2000,
114
+ "Office Depot": 2000, "PetSmart": 1800, "Petco": 1400, "Trader Joe's": 1200,
115
+ "Aldi": 1500, "Lidl": 1500, "Ace Hardware": 800, "DSW Designer Shoe Warehouse": 2000
116
+ }
117
+
118
+ # FULL LIST AS REQUESTED
119
+ SEARCH_LIST = [
120
+ "Walmart", "Target", "Kmart", "Sears", "Kohl's", "Macy's", "JCPenney",
121
+ "TJX", "TJX Companies", "T.J. Maxx", "Marshalls", "HomeGoods", "HomeSense",
122
+ "Ross", "Ross Dress for Less", "Burlington", "Dick's Sporting Goods",
123
+ "Albertsons", "Safeway", "Home Depot", "Lowe's", "Best Buy",
124
+ "IKEA", "Bob's Furniture", "Bob's Discount Furniture", "Raymour & Flanigan",
125
+ "Barnes & Noble", "Office Depot", "OfficeMax", "Staples", "Lowe",
126
+ "PetSmart", "Petco", "Kroger", "Meijer", "Costco", "BJ's Wholesale Club",
127
+ "Sam's Club", "Whole Foods", "ShopRite", "Stop & Shop", "Trader Joe's",
128
+ "Michaels", "Lidl", "Aldi", "DSW Designer Shoe Warehouse", "Old Navy",
129
+ "Ace", "Ace Hardware", "Hobby Lobby", "Trader Joes"
130
+ ]
131
+
132
+ # ==========================================
133
+ # MAIN LOGIC WITH GENERATOR (YIELD)
134
+ # ==========================================
135
+ def process_data(file_obj):
136
+ if not API_KEY:
137
+ yield "❌ API Key not found! Set GOOGLE_API_KEY in Secrets.", None
138
+ return
139
+
140
+ if file_obj is None:
141
+ yield "❌ Please upload a file.", None
142
+ return
143
+
144
+ yield "πŸ“‚ Loading Polygon...", None
145
+ polygon = load_geodata_to_polygon(file_obj)
146
+
147
+ if not polygon:
148
+ yield "❌ Failed to read KML/KMZ file.", None
149
+ return
150
+
151
+ # --- AREA LIMIT CHECK (250M sq meters) ---
152
+ try:
153
+ gs = gpd.GeoSeries([polygon], crs="EPSG:4326")
154
+ gs_proj = gs.to_crs(epsg=6933)
155
+ area_sq_meters = gs_proj.area.iloc[0]
156
+ limit_sq_meters = 250_000_000
157
+
158
+ if area_sq_meters > limit_sq_meters:
159
+ yield f"⚠️ AREA TOO LARGE: {area_sq_meters:,.0f} sq m. (Limit: {limit_sq_meters:,.0f}). Upload a smaller file.", None
160
+ return
161
+ except:
162
+ pass
163
+
164
+ gmaps = googlemaps.Client(key=API_KEY)
165
+ results = []
166
+ seen_ids = set()
167
+ total_brands = len(SEARCH_LIST)
168
+
169
+ # LOOP THROUGH BRANDS
170
+ for i, brand in enumerate(SEARCH_LIST):
171
+ yield f"πŸ” Scanning Brand {i+1}/{total_brands}: {brand}...", None
172
+
173
+ try:
174
+ # 1. INITIAL SEARCH (Page 1 - Top 20)
175
+ # 10km radius ensures we catch local stores without searching the whole world
176
+ places = gmaps.places_nearby(
177
+ location=(polygon.centroid.y, polygon.centroid.x),
178
+ radius=10000,
179
+ keyword=brand
180
+ )
181
+
182
+ all_results = places.get('results', [])
183
+
184
+ # 2. PAGINATION (THE FIX for "Top 20 Trap")
185
+ # If Google says "there is more", we wait and ask for Page 2 and Page 3.
186
+ # This gets us up to 60 results, ensuring we don't miss the Meijer.
187
+ while 'next_page_token' in places:
188
+ time.sleep(2) # Mandatory delay for Google API
189
+ places = gmaps.places_nearby(
190
+ location=(polygon.centroid.y, polygon.centroid.x),
191
+ radius=10000,
192
+ keyword=brand,
193
+ page_token=places['next_page_token']
194
+ )
195
+ all_results.extend(places.get('results', []))
196
+
197
+ # 3. STRICT FILTER
198
+ for p in all_results:
199
+ pid = p.get('place_id')
200
+ if pid in seen_ids: continue
201
+
202
+ name = p.get('name')
203
+ if any(x in name for x in ["ATM", "Redbox", "Kiosk", "Coinme", "Gas", "Fuel"]): continue
204
+
205
+ lat = p['geometry']['location']['lat']
206
+ lng = p['geometry']['location']['lng']
207
+
208
+ # STRICT CONTAINMENT: Only Keep if INSIDE the KML
209
+ if not polygon.contains(Point(lng, lat)): continue
210
+ seen_ids.add(pid)
211
+
212
+ # Get Data
213
+ roof_area = get_roof_area(lat, lng, API_KEY)
214
+ height, floors = get_osm_physics(lat, lng)
215
+
216
+ # Logic
217
+ source_note = "SolarAPI"
218
+ if roof_area is None:
219
+ roof_area = BRAND_AVG_AREA.get(brand, 2500.0)
220
+ source_note = "Brand_Avg (Missing)"
221
+ else:
222
+ if roof_area > 30000 and brand not in ["IKEA", "Costco", "Meijer", "Sam's Club", "Walmart"]:
223
+ roof_area = BRAND_AVG_AREA.get(brand, 2500.0)
224
+ source_note = "Brand_Avg (Mall detected)"
225
+ elif roof_area < 500 and brand not in ["Ace Hardware", "Trader Joe's", "GameStop"]:
226
+ roof_area = BRAND_AVG_AREA.get(brand, 2500.0)
227
+ source_note = "Brand_Avg (Too Small detected)"
228
+
229
+ if floors is None: floors = BRAND_FLOORS.get(brand, 1)
230
+ if height is None: height = floors * 6.0
231
+
232
+ results.append({
233
+ 'Name': name, 'Brand': brand, 'Latitude': lat, 'Longitude': lng,
234
+ 'Height_m': round(height, 2), 'Num_Floors': int(floors),
235
+ 'Area_sqm': round(roof_area, 2), 'Data_Source': source_note
236
+ })
237
+ except:
238
+ pass
239
+
240
+ if not results:
241
+ yield "❌ No stores found in this area.", None
242
+ return
243
+
244
+ df = pd.DataFrame(results)
245
+ bad_keywords = ['Mobile', 'Salon', 'Floral', 'Bakery', 'Pharmacy', 'Optical', 'Geek Squad', 'Photo']
246
+ df = df[~df['Name'].str.contains('|'.join(bad_keywords), case=False, na=False)]
247
+
248
+ output_path = "Building_Inventory.csv"
249
+ df.to_csv(output_path, index=False)
250
+
251
+ yield f"βœ… Success! Found {len(df)} stores.", output_path
252
+
253
+ # ==========================================
254
+ # GRADIO INTERFACE
255
+ # ==========================================
256
+ iface = gr.Interface(
257
+ fn=process_data,
258
+ inputs=gr.File(label="Upload Polygon (Limit: 250,000,000 sq m)"),
259
+ outputs=[
260
+ gr.Textbox(label="Status Log"),
261
+ gr.File(label="Download CSV")
262
+ ],
263
+ title="πŸ™οΈ Commercial Building Inventory Generator",
264
+ description="Upload a KMZ file. The tool will scan for major Big Box brands, check Solar API for area, and OpenStreetMap for height/floors."
265
+ )
266
+
267
+ iface.launch()