forever-sheikh commited on
Commit
681d534
Β·
verified Β·
1 Parent(s): 3d5ee46

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +263 -0
app.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 (Remove Z-axis) ---
46
+ # KMLs often have altitude (x, y, z). We strictly want (x, y).
47
+ def force_2d(geometry):
48
+ if geometry.has_z:
49
+ return transform(lambda x, y, z=None: (x, y), geometry)
50
+ return geometry
51
+
52
+ gdf.geometry = gdf.geometry.apply(force_2d)
53
+ # ---------------------------------------
54
+
55
+ return gdf.union_all()
56
+ except Exception as e:
57
+ print(f"Error loading KML: {e}")
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
+ SEARCH_LIST = [
119
+ "Walmart", "Target", "Kmart", "Sears", "Kohl's", "Macy's", "JCPenney",
120
+ "T.J. Maxx", "Marshalls", "HomeGoods", "HomeSense", "Ross", "Ross Dress for Less", "Burlington",
121
+ "Dick's Sporting Goods", "Albertsons", "Safeway", "Home Depot", "Lowe's",
122
+ "Best Buy", "IKEA", "Bob's Discount Furniture", "Raymour & Flanigan",
123
+ "Barnes & Noble", "Office Depot", "OfficeMax", "Staples", "Lowe",
124
+ "PetSmart", "Petco", "Kroger", "Meijer", "Costco", "BJ's Wholesale Club",
125
+ "Sam's Club", "Whole Foods", "ShopRite", "Stop & Shop", "Trader Joe's",
126
+ "Lidl", "Aldi", "DSW Designer Shoe Warehouse", "Old Navy", "Ace Hardware", "Michaels",
127
+ "Hobby Lobby", "Trader Joes"
128
+ ]
129
+
130
+ # ==========================================
131
+ # MAIN LOGIC WITH GENERATOR (YIELD)
132
+ # ==========================================
133
+ def process_data(file_obj):
134
+ # This allows real-time updates to the UI
135
+ if not API_KEY:
136
+ yield "❌ API Key not found! Set GOOGLE_API_KEY in Secrets.", None
137
+ return
138
+
139
+ if file_obj is None:
140
+ yield "❌ Please upload a file.", None
141
+ return
142
+
143
+ yield "πŸ“‚ Loading Polygon...", None
144
+ polygon = load_geodata_to_polygon(file_obj)
145
+
146
+ if not polygon:
147
+ yield "❌ Failed to read KML/KMZ file.", None
148
+ return
149
+
150
+ # --- CHECK AREA SIZE (With Error Handling) ---
151
+ try:
152
+ # Check size on 2D version
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
+
162
+ yield f"βœ… Area: {area_sq_meters:,.0f} sq m. Starting scan...", None
163
+
164
+ except Exception as e:
165
+ print(f"Area Check Error: {e}")
166
+ yield "⚠️ Area check skipped (System Error). Proceeding...", None
167
+ # ---------------------------------------------
168
+
169
+ # --- FIX 2: CREATE A BUFFER ZONE ---
170
+ # This expands the polygon by ~15 meters (0.00015 degrees)
171
+ # This catches stores where the pin is slightly outside the line.
172
+ search_polygon = polygon.buffer(0.00015)
173
+ # -----------------------------------
174
+
175
+ gmaps = googlemaps.Client(key=API_KEY)
176
+ results = []
177
+ seen_ids = set()
178
+ total_brands = len(SEARCH_LIST)
179
+
180
+ # LOOP THROUGH BRANDS
181
+ for i, brand in enumerate(SEARCH_LIST):
182
+ yield f"πŸ” Scanning Brand {i+1}/{total_brands}: {brand}...", None
183
+
184
+ try:
185
+ places = gmaps.places_nearby(
186
+ location=(polygon.centroid.y, polygon.centroid.x),
187
+ radius=50000,
188
+ keyword=brand
189
+ )
190
+
191
+ for p in places.get('results', []):
192
+ pid = p.get('place_id')
193
+ if pid in seen_ids: continue
194
+
195
+ name = p.get('name')
196
+ if any(x in name for x in ["ATM", "Redbox", "Kiosk", "Coinme", "Gas", "Fuel"]): continue
197
+
198
+ lat = p['geometry']['location']['lat']
199
+ lng = p['geometry']['location']['lng']
200
+
201
+ # USE THE BUFFERED POLYGON FOR THE CHECK
202
+ if not search_polygon.contains(Point(lng, lat)): continue
203
+ seen_ids.add(pid)
204
+
205
+ # Get Data
206
+ roof_area = get_roof_area(lat, lng, API_KEY)
207
+ height, floors = get_osm_physics(lat, lng)
208
+
209
+ # Logic
210
+ source_note = "SolarAPI"
211
+ if roof_area is None:
212
+ roof_area = BRAND_AVG_AREA.get(brand, 2500.0)
213
+ source_note = "Brand_Avg (Missing)"
214
+ else:
215
+ if roof_area > 30000 and brand not in ["IKEA", "Costco", "Meijer", "Sam's Club", "Walmart"]:
216
+ roof_area = BRAND_AVG_AREA.get(brand, 2500.0)
217
+ source_note = "Brand_Avg (Mall detected)"
218
+ elif roof_area < 500 and brand not in ["Ace Hardware", "Trader Joe's", "GameStop"]:
219
+ roof_area = BRAND_AVG_AREA.get(brand, 2500.0)
220
+ source_note = "Brand_Avg (Too Small detected)"
221
+
222
+ if floors is None: floors = BRAND_FLOORS.get(brand, 1)
223
+ if height is None: height = floors * 6.0
224
+
225
+ results.append({
226
+ 'Name': name, 'Brand': brand, 'Latitude': lat, 'Longitude': lng,
227
+ 'Height_m': round(height, 2), 'Num_Floors': int(floors),
228
+ 'Area_sqm': round(roof_area, 2), 'Data_Source': source_note
229
+ })
230
+ except Exception as e:
231
+ print(f"❌ Error scanning {brand}: {str(e)}")
232
+ pass
233
+
234
+ # FINISHED
235
+ if not results:
236
+ yield "❌ No stores found in this area.", None
237
+ return
238
+
239
+ # Export
240
+ df = pd.DataFrame(results)
241
+ bad_keywords = ['Mobile', 'Salon', 'Floral', 'Bakery', 'Pharmacy', 'Optical', 'Geek Squad', 'Photo']
242
+ df = df[~df['Name'].str.contains('|'.join(bad_keywords), case=False, na=False)]
243
+
244
+ output_path = "Building_Inventory.csv"
245
+ df.to_csv(output_path, index=False)
246
+
247
+ yield f"βœ… Success! Found {len(df)} stores.", output_path
248
+
249
+ # ==========================================
250
+ # GRADIO INTERFACE
251
+ # ==========================================
252
+ iface = gr.Interface(
253
+ fn=process_data,
254
+ inputs=gr.File(label="Upload Polygon (Limit: 250,000,000 sq m)"),
255
+ outputs=[
256
+ gr.Textbox(label="Status Log"),
257
+ gr.File(label="Download CSV")
258
+ ],
259
+ title="πŸ™οΈ Commercial Building Inventory Generator",
260
+ description="Upload a KMZ file. The tool will scan for over 45 Big Box brands, check Solar API for area, and OpenStreetMap for height/floors."
261
+ )
262
+
263
+ iface.launch()