cwadayi commited on
Commit
3bd2b2f
·
verified ·
1 Parent(s): 9bdbe5b

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +239 -0
app.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import time
4
+ import tempfile
5
+ from typing import Optional, Tuple, List
6
+
7
+ import matplotlib.pyplot as plt
8
+ import gradio as gr
9
+
10
+ # Google Drive (service account)
11
+ from googleapiclient.discovery import build
12
+ from googleapiclient.http import MediaIoBaseUpload
13
+ from google.oauth2.service_account import Credentials
14
+
15
+
16
+ # --------- Config ---------
17
+ DEFAULT_EXTENT = (119.0, 123.5, 21.5, 25.5) # (min_lon, max_lon, min_lat, max_lat) ~ Taiwan region
18
+ SCOPES = ["https://www.googleapis.com/auth/drive.file"] # minimal scope to create files you own
19
+
20
+
21
+ def _parse_points(csv_text: str) -> List[Tuple[float, float]]:
22
+ """
23
+ Accepts lines like:
24
+ 121.5,25.0
25
+ 120.9,24.5
26
+ 121.0,23.9
27
+ (lon,lat). Ignores blank/comment lines.
28
+ """
29
+ pts = []
30
+ if not csv_text:
31
+ return pts
32
+ for ln in csv_text.strip().splitlines():
33
+ ln = ln.strip()
34
+ if not ln or ln.startswith("#"):
35
+ continue
36
+ # allow "lon,lat" or "lat,lon" if user toggles a flag later (we keep lon,lat only here for simplicity)
37
+ parts = [p.strip() for p in ln.replace("\t", ",").split(",")]
38
+ if len(parts) < 2:
39
+ continue
40
+ try:
41
+ lon = float(parts[0])
42
+ lat = float(parts[1])
43
+ pts.append((lon, lat))
44
+ except ValueError:
45
+ # skip invalid rows
46
+ continue
47
+ return pts
48
+
49
+
50
+ def _make_figure(
51
+ points: List[Tuple[float, float]],
52
+ title: str,
53
+ extent: Tuple[float, float, float, float] = DEFAULT_EXTENT,
54
+ dpi: int = 150,
55
+ ) -> str:
56
+ """
57
+ Draw a simple lon/lat scatter within the extent.
58
+ Returns path to the saved PNG.
59
+ """
60
+ min_lon, max_lon, min_lat, max_lat = extent
61
+ fig, ax = plt.subplots(figsize=(6.5, 6.5), dpi=dpi)
62
+ ax.set_title(title or "Map Plot", pad=10)
63
+ ax.set_xlabel("Longitude")
64
+ ax.set_ylabel("Latitude")
65
+
66
+ # Axes extent
67
+ ax.set_xlim(min_lon, max_lon)
68
+ ax.set_ylim(min_lat, max_lat)
69
+ ax.grid(True, linestyle=":", linewidth=0.8)
70
+
71
+ # Light background box suggesting region
72
+ rect = plt.Rectangle(
73
+ (min_lon, min_lat),
74
+ max_lon - min_lon,
75
+ max_lat - min_lat,
76
+ fill=False,
77
+ linestyle="--",
78
+ linewidth=1.0,
79
+ )
80
+ ax.add_patch(rect)
81
+
82
+ # Plot points
83
+ if points:
84
+ xs = [p[0] for p in points]
85
+ ys = [p[1] for p in points]
86
+ ax.scatter(xs, ys, s=60, marker="*", label="Input points")
87
+ ax.legend(loc="upper right")
88
+
89
+ # Save to a temporary PNG
90
+ tmpdir = tempfile.gettempdir()
91
+ ts = int(time.time())
92
+ filename = f"map_{ts}.png"
93
+ out_path = os.path.join(tmpdir, filename)
94
+ fig.tight_layout()
95
+ fig.savefig(out_path)
96
+ plt.close(fig)
97
+ return out_path
98
+
99
+
100
+ def _drive_service_from_env() -> Optional[object]:
101
+ """
102
+ Build a Google Drive service using a service account JSON
103
+ provided via the Hugging Face Space secret `SERVICE_ACCOUNT_JSON`.
104
+ """
105
+ sa_json = os.environ.get("SERVICE_ACCOUNT_JSON", "").strip()
106
+ if not sa_json:
107
+ return None
108
+
109
+ # In Spaces secrets, you typically paste the **full** JSON string of the service account.
110
+ # Load it into Credentials directly via from_service_account_info.
111
+ import json
112
+ try:
113
+ info = json.loads(sa_json)
114
+ except Exception:
115
+ return None
116
+
117
+ creds = Credentials.from_service_account_info(info, scopes=SCOPES)
118
+ service = build("drive", "v3", credentials=creds, cache_discovery=False)
119
+ return service
120
+
121
+
122
+ def _upload_to_drive(
123
+ local_path: str,
124
+ service,
125
+ folder_id: Optional[str] = None,
126
+ ) -> Tuple[str, str]:
127
+ """
128
+ Uploads local file to Google Drive.
129
+ Returns (file_id, webViewLink)
130
+ """
131
+ fname = os.path.basename(local_path)
132
+ file_metadata = {"name": fname}
133
+ if folder_id:
134
+ file_metadata["parents"] = [folder_id]
135
+
136
+ with open(local_path, "rb") as f:
137
+ media = MediaIoBaseUpload(f, mimetype="image/png", resumable=False)
138
+ created = (
139
+ service.files()
140
+ .create(body=file_metadata, media_body=media, fields="id, webViewLink, webContentLink")
141
+ .execute()
142
+ )
143
+
144
+ file_id = created.get("id", "")
145
+ web_view_link = created.get("webViewLink", "")
146
+ return file_id, web_view_link
147
+
148
+
149
+ def generate_and_upload(
150
+ points_text: str,
151
+ title: str,
152
+ min_lon: float,
153
+ max_lon: float,
154
+ min_lat: float,
155
+ max_lat: float,
156
+ drive_folder_id_input: str,
157
+ ):
158
+ """
159
+ Gradio handler:
160
+ 1) parse points
161
+ 2) make figure
162
+ 3) upload to Drive (if configured)
163
+ 4) return preview image and link
164
+ """
165
+ points = _parse_points(points_text)
166
+ extent = (min_lon, max_lon, min_lat, max_lat)
167
+ local_png = _make_figure(points, title, extent=extent)
168
+
169
+ # Build Drive service from env
170
+ service = _drive_service_from_env()
171
+ if service is None:
172
+ # Tell user how to set up secrets
173
+ msg = (
174
+ "⚠️ Google Drive upload is not configured. "
175
+ "Please add a Hugging Face Space secret named `SERVICE_ACCOUNT_JSON` "
176
+ "with your service account JSON. Optionally add `DRIVE_FOLDER_ID`."
177
+ )
178
+ # Return just the preview, no link
179
+ return local_png, msg
180
+
181
+ # Determine folder ID (priority: user input > env)
182
+ folder_id_env = os.environ.get("DRIVE_FOLDER_ID", "").strip()
183
+ folder_id = (drive_folder_id_input or folder_id_env or "").strip() or None
184
+
185
+ try:
186
+ file_id, view_link = _upload_to_drive(local_png, service, folder_id=folder_id)
187
+ link_text = f"✅ Uploaded to Google Drive.\n• File ID: `{file_id}`\n• Open: {view_link}"
188
+ return local_png, link_text
189
+ except Exception as e:
190
+ return local_png, f"❌ Upload failed: {e}"
191
+
192
+
193
+ # ----------------- Gradio UI -----------------
194
+ with gr.Blocks(title="Map → Google Drive Uploader") as demo:
195
+ gr.Markdown(
196
+ """
197
+ # 🗺️ Map → Google Drive Uploader
198
+ Paste lon,lat pairs (one per line), set the extent, and click **Generate & Upload**.
199
+
200
+ **Note:** To enable Google Drive upload in this Space, add the secret `SERVICE_ACCOUNT_JSON` (full service account JSON)
201
+ and optionally `DRIVE_FOLDER_ID` in the Space settings.
202
+ """
203
+ )
204
+
205
+ with gr.Row():
206
+ with gr.Column():
207
+ points_in = gr.Textbox(
208
+ label="Points (lon,lat per line)",
209
+ value="121.5,25.0\n120.9,24.5\n121.0,23.9",
210
+ lines=8,
211
+ placeholder="e.g.\n121.5,25.0\n120.9,24.5",
212
+ )
213
+ title_in = gr.Textbox(label="Map title", value="Taiwan Points Example")
214
+
215
+ with gr.Accordion("Map extent (lon/lat bounds)", open=False):
216
+ min_lon_in = gr.Number(label="min_lon", value=DEFAULT_EXTENT[0])
217
+ max_lon_in = gr.Number(label="max_lon", value=DEFAULT_EXTENT[1])
218
+ min_lat_in = gr.Number(label="min_lat", value=DEFAULT_EXTENT[2])
219
+ max_lat_in = gr.Number(label="max_lat", value=DEFAULT_EXTENT[3])
220
+
221
+ folder_id_in = gr.Textbox(
222
+ label="Google Drive Folder ID (optional — overrides Space secret)",
223
+ placeholder="e.g. 1AbCdEfGhIJKlmnopQRstuVWxyz",
224
+ )
225
+
226
+ run_btn = gr.Button("🚀 Generate & Upload")
227
+
228
+ with gr.Column():
229
+ out_image = gr.Image(label="Preview", type="filepath")
230
+ out_msg = gr.Markdown(label="Result / Link")
231
+
232
+ run_btn.click(
233
+ fn=generate_and_upload,
234
+ inputs=[points_in, title_in, min_lon_in, max_lon_in, min_lat_in, max_lat_in, folder_id_in],
235
+ outputs=[out_image, out_msg],
236
+ )
237
+
238
+ if __name__ == "__main__":
239
+ demo.launch()