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.
Files changed (30) hide show
  1. {rsplot-0.2.1 → rsplot-0.2.2}/PKG-INFO +1 -1
  2. {rsplot-0.2.1 → rsplot-0.2.2}/pyproject.toml +1 -1
  3. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/cli.py +1 -0
  4. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/results.py +91 -13
  5. {rsplot-0.2.1 → rsplot-0.2.2}/.gitignore +0 -0
  6. {rsplot-0.2.1 → rsplot-0.2.2}/LICENSE +0 -0
  7. {rsplot-0.2.1 → rsplot-0.2.2}/README.md +0 -0
  8. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/__init__.py +0 -0
  9. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/__main__.py +0 -0
  10. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/config.py +0 -0
  11. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/fnr.py +0 -0
  12. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/geo/__init__.py +0 -0
  13. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/geo/boundaries.py +0 -0
  14. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/geo/gridding.py +0 -0
  15. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/__init__.py +0 -0
  16. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/colormaps.py +0 -0
  17. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/fnr.py +0 -0
  18. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/overlay.py +0 -0
  19. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/raster.py +0 -0
  20. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/station.py +0 -0
  21. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/plotting/styles.py +0 -0
  22. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/__init__.py +0 -0
  23. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/base.py +0 -0
  24. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/guokong.py +0 -0
  25. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/tropomi_hcho.py +0 -0
  26. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/tropomi_no2.py +0 -0
  27. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/readers/tropomi_o3.py +0 -0
  28. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/recent.py +0 -0
  29. {rsplot-0.2.1 → rsplot-0.2.2}/src/rsplot/tiles/__init__.py +0 -0
  30. {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.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "rsplot"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "CLI tool for plotting Remote Sensing data, designed for Agent."
5
5
  authors = [
6
6
  { name = "Yaoyao Hu", email = "yaoyhu@mail.ustc.edu.cn" },
@@ -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 = cities_gdf.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
- city = ""
209
+ area = ""
200
210
  for idx in sindex.intersection((x, y, x, y)):
201
- if cities_gdf.iloc[idx].geometry.contains(pt):
202
- city = cities_gdf.iloc[idx]["name"]
211
+ if areas_gdf.iloc[idx].geometry.contains(pt):
212
+ area = areas_gdf.iloc[idx][name_field]
203
213
  break
204
- out[i] = city
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
- station_cities = None
257
- if cities_gdf is not None and len(cities_gdf) > 0 and len(vals) > 0:
258
- station_cities = _attribute_stations_to_cities(lons, lats, cities_gdf)
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 station_cities is not None:
276
- entry["city"] = station_cities[i] or None
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-city ranking
291
- if station_cities is not None and len(vals) > 0:
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 = station_cities[i]
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