rsplot 0.2.1__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rsplot-0.2.1 → rsplot-0.2.2}/PKG-INFO +1 -1
- {rsplot-0.2.1 → rsplot-0.2.2}/pyproject.toml +1 -1
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/cli.py +1 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/results.py +91 -13
- {rsplot-0.2.1 → rsplot-0.2.2}/.gitignore +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/LICENSE +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/README.md +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/__init__.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/__main__.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/config.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/fnr.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/geo/__init__.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/geo/boundaries.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/geo/gridding.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/__init__.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/colormaps.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/fnr.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/overlay.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/raster.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/station.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/styles.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/__init__.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/base.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/guokong.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/tropomi_hcho.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/tropomi_no2.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/tropomi_o3.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/recent.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/tiles/__init__.py +0 -0
- {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/tiles/tianditu.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rsplot
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: CLI tool for plotting Remote Sensing data, designed for Agent.
|
|
5
5
|
Project-URL: Repository, https://git.lug.ustc.edu.cn/yaoyhu/rsplot
|
|
6
6
|
Project-URL: GitHub, https://github.com/yaoyhu/rsplot
|
|
@@ -496,6 +496,7 @@ def station(
|
|
|
496
496
|
console.print(f"[bold]解析区域:[/bold] {region}")
|
|
497
497
|
info = resolve_region(region, cfg, level_override=level)
|
|
498
498
|
console.print(f" → [green]{info.name}[/green] (级别: {info.level})")
|
|
499
|
+
params = info.params
|
|
499
500
|
|
|
500
501
|
# --- 4. Read station data ---
|
|
501
502
|
use_data_dir = (
|
|
@@ -187,21 +187,31 @@ def _attribute_stations_to_cities(
|
|
|
187
187
|
lon: np.ndarray, lat: np.ndarray, cities_gdf: gpd.GeoDataFrame
|
|
188
188
|
) -> np.ndarray:
|
|
189
189
|
"""Return city name for each (lon, lat); empty string if none contain it."""
|
|
190
|
+
return _attribute_stations_to_areas(lon, lat, cities_gdf)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _attribute_stations_to_areas(
|
|
194
|
+
lon: np.ndarray,
|
|
195
|
+
lat: np.ndarray,
|
|
196
|
+
areas_gdf: gpd.GeoDataFrame,
|
|
197
|
+
name_field: str = "name",
|
|
198
|
+
) -> np.ndarray:
|
|
199
|
+
"""Return containing admin-unit name for each (lon, lat)."""
|
|
190
200
|
from shapely.geometry import Point
|
|
191
201
|
|
|
192
|
-
sindex =
|
|
202
|
+
sindex = areas_gdf.sindex
|
|
193
203
|
out = np.empty(len(lon), dtype=object)
|
|
194
204
|
for i, (x, y) in enumerate(zip(lon, lat)):
|
|
195
205
|
if not (np.isfinite(x) and np.isfinite(y)):
|
|
196
206
|
out[i] = ""
|
|
197
207
|
continue
|
|
198
208
|
pt = Point(float(x), float(y))
|
|
199
|
-
|
|
209
|
+
area = ""
|
|
200
210
|
for idx in sindex.intersection((x, y, x, y)):
|
|
201
|
-
if
|
|
202
|
-
|
|
211
|
+
if areas_gdf.iloc[idx].geometry.contains(pt):
|
|
212
|
+
area = areas_gdf.iloc[idx][name_field]
|
|
203
213
|
break
|
|
204
|
-
out[i] =
|
|
214
|
+
out[i] = area
|
|
205
215
|
return out
|
|
206
216
|
|
|
207
217
|
|
|
@@ -220,6 +230,46 @@ def _aqi_distribution(vals: np.ndarray) -> dict[str, int]:
|
|
|
220
230
|
return dist
|
|
221
231
|
|
|
222
232
|
|
|
233
|
+
def _station_admin_level(region: RegionInfo) -> str | None:
|
|
234
|
+
if region.level == "country":
|
|
235
|
+
return "province"
|
|
236
|
+
if region.level == "key_region":
|
|
237
|
+
return "city"
|
|
238
|
+
if region.level == "province":
|
|
239
|
+
municipalities = {"北京市", "天津市", "上海市", "重庆市"}
|
|
240
|
+
return "county" if region.name in municipalities else "city"
|
|
241
|
+
if region.level == "city":
|
|
242
|
+
return "county"
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _top_station_entries(
|
|
247
|
+
*,
|
|
248
|
+
vals: np.ndarray,
|
|
249
|
+
ids: np.ndarray,
|
|
250
|
+
lons: np.ndarray,
|
|
251
|
+
lats: np.ndarray,
|
|
252
|
+
area_names: np.ndarray | None,
|
|
253
|
+
admin_level: str | None,
|
|
254
|
+
limit: int = 10,
|
|
255
|
+
) -> list[dict[str, Any]]:
|
|
256
|
+
top_stations: list[dict[str, Any]] = []
|
|
257
|
+
for i in np.argsort(vals)[::-1][:limit]:
|
|
258
|
+
entry: dict[str, Any] = {
|
|
259
|
+
"id": str(ids[i]),
|
|
260
|
+
"value": _round(vals[i]),
|
|
261
|
+
"lon": round(float(lons[i]), 3),
|
|
262
|
+
"lat": round(float(lats[i]), 3),
|
|
263
|
+
}
|
|
264
|
+
if area_names is not None:
|
|
265
|
+
entry["admin_level"] = admin_level
|
|
266
|
+
entry["admin_name"] = area_names[i] or None
|
|
267
|
+
# Backward-compatible alias for older consumers.
|
|
268
|
+
entry["city"] = area_names[i] or None
|
|
269
|
+
top_stations.append(entry)
|
|
270
|
+
return top_stations
|
|
271
|
+
|
|
272
|
+
|
|
223
273
|
def build_station_result(
|
|
224
274
|
*,
|
|
225
275
|
region: RegionInfo,
|
|
@@ -235,6 +285,12 @@ def build_station_result(
|
|
|
235
285
|
lons = data.lon[valid]
|
|
236
286
|
lats = data.lat[valid]
|
|
237
287
|
ids = np.asarray(data.id)[valid]
|
|
288
|
+
admin_gdf = (
|
|
289
|
+
region.sub_boundary_gdf
|
|
290
|
+
if region.sub_boundary_gdf is not None and len(region.sub_boundary_gdf) > 0
|
|
291
|
+
else cities_gdf
|
|
292
|
+
)
|
|
293
|
+
admin_level = _station_admin_level(region)
|
|
238
294
|
|
|
239
295
|
result: dict[str, Any] = {
|
|
240
296
|
"command": "station",
|
|
@@ -253,9 +309,25 @@ def build_station_result(
|
|
|
253
309
|
"stats": _basic_stats(vals),
|
|
254
310
|
}
|
|
255
311
|
|
|
256
|
-
|
|
257
|
-
if
|
|
258
|
-
|
|
312
|
+
station_areas = None
|
|
313
|
+
if admin_gdf is not None and len(admin_gdf) > 0 and len(vals) > 0:
|
|
314
|
+
station_areas = _attribute_stations_to_areas(
|
|
315
|
+
lons, lats, admin_gdf, region.sub_name_field
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if len(vals) > 0:
|
|
319
|
+
result["top_stations"] = _top_station_entries(
|
|
320
|
+
vals=vals,
|
|
321
|
+
ids=ids,
|
|
322
|
+
lons=lons,
|
|
323
|
+
lats=lats,
|
|
324
|
+
area_names=station_areas,
|
|
325
|
+
admin_level=admin_level,
|
|
326
|
+
limit=10,
|
|
327
|
+
)
|
|
328
|
+
result["top_station_ids"] = [
|
|
329
|
+
entry["id"] for entry in result["top_stations"]
|
|
330
|
+
]
|
|
259
331
|
|
|
260
332
|
# Exceedance summary
|
|
261
333
|
if threshold is not None and len(vals) > 0:
|
|
@@ -272,8 +344,11 @@ def build_station_result(
|
|
|
272
344
|
"lon": round(float(lons[i]), 3),
|
|
273
345
|
"lat": round(float(lats[i]), 3),
|
|
274
346
|
}
|
|
275
|
-
if
|
|
276
|
-
entry["
|
|
347
|
+
if station_areas is not None:
|
|
348
|
+
entry["admin_level"] = admin_level
|
|
349
|
+
entry["admin_name"] = station_areas[i] or None
|
|
350
|
+
# Backward-compatible alias for older consumers.
|
|
351
|
+
entry["city"] = station_areas[i] or None
|
|
277
352
|
top_stations.append(entry)
|
|
278
353
|
if len(top_stations) >= 10:
|
|
279
354
|
break
|
|
@@ -287,11 +362,11 @@ def build_station_result(
|
|
|
287
362
|
if data.var_name == "AQI" and len(vals) > 0:
|
|
288
363
|
result["aqi_distribution"] = _aqi_distribution(vals)
|
|
289
364
|
|
|
290
|
-
# Per-
|
|
291
|
-
if
|
|
365
|
+
# Per-admin-unit ranking, exposed under the legacy city_ranking key.
|
|
366
|
+
if station_areas is not None and len(vals) > 0:
|
|
292
367
|
acc: dict[str, dict[str, Any]] = {}
|
|
293
368
|
for i in range(len(vals)):
|
|
294
|
-
c =
|
|
369
|
+
c = station_areas[i]
|
|
295
370
|
if not c:
|
|
296
371
|
continue
|
|
297
372
|
s = acc.setdefault(c, {"values": [], "n_exceed": 0})
|
|
@@ -303,6 +378,9 @@ def build_station_result(
|
|
|
303
378
|
arr = np.asarray(s["values"])
|
|
304
379
|
ranking.append(
|
|
305
380
|
{
|
|
381
|
+
"name": city,
|
|
382
|
+
"admin_level": admin_level,
|
|
383
|
+
# Backward-compatible alias for older consumers.
|
|
306
384
|
"city": city,
|
|
307
385
|
"n_valid": len(arr),
|
|
308
386
|
"mean": _round(np.mean(arr)),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|