forever-sheikh commited on
Commit
dbe8033
·
verified ·
1 Parent(s): ba79584

Update app.py

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