ssb-sgis 1.0.12__py3-none-any.whl → 1.0.14__py3-none-any.whl

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.
@@ -0,0 +1,165 @@
1
+ import datetime
2
+ import json
3
+ import re
4
+ from collections.abc import Iterable
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from urllib.request import urlopen
9
+
10
+ import folium
11
+ import shapely
12
+
13
+ from ..geopandas_tools.conversion import to_shapely
14
+
15
+ JSON_PATH = Path(__file__).parent / "norge_i_bilder.json"
16
+
17
+ DEFAULT_YEARS: tuple[str] = tuple(
18
+ str(year)
19
+ for year in range(
20
+ int(datetime.datetime.now().year) - 8,
21
+ int(datetime.datetime.now().year) + 1,
22
+ )
23
+ )
24
+
25
+
26
+ @dataclass
27
+ class NorgeIBilderWms:
28
+ """Loads Norge i bilder tiles as folium.WmsTiles."""
29
+
30
+ years: Iterable[int | str] = DEFAULT_YEARS
31
+ contains: str | Iterable[str] | None = None
32
+ not_contains: str | Iterable[str] | None = None
33
+
34
+ def load_tiles(self) -> None:
35
+ """Load all Norge i bilder tiles into self.tiles."""
36
+ url = "https://wms.geonorge.no/skwms1/wms.nib-prosjekter?SERVICE=WMS&REQUEST=GetCapabilities"
37
+
38
+ name_pattern = r"<Name>(.*?)</Name>"
39
+ bbox_pattern = (
40
+ r"<EX_GeographicBoundingBox>.*?"
41
+ r"<westBoundLongitude>(.*?)</westBoundLongitude>.*?"
42
+ r"<eastBoundLongitude>(.*?)</eastBoundLongitude>.*?"
43
+ r"<southBoundLatitude>(.*?)</southBoundLatitude>.*?"
44
+ r"<northBoundLatitude>(.*?)</northBoundLatitude>.*?</EX_GeographicBoundingBox>"
45
+ )
46
+
47
+ all_tiles: list[dict] = []
48
+ with urlopen(url) as file:
49
+ xml_data: str = file.read().decode("utf-8")
50
+
51
+ for text in xml_data.split('<Layer queryable="1">')[1:]:
52
+
53
+ # Extract bounding box values
54
+ bbox_match = re.search(bbox_pattern, text, re.DOTALL)
55
+ if bbox_match:
56
+ minx, maxx, miny, maxy = (
57
+ float(bbox_match.group(i)) for i in [1, 2, 3, 4]
58
+ )
59
+ this_bbox = shapely.box(minx, miny, maxx, maxy)
60
+ else:
61
+ this_bbox = None
62
+
63
+ name_match = re.search(name_pattern, text, re.DOTALL)
64
+ name = name_match.group(1) if name_match else None
65
+
66
+ if (
67
+ not name
68
+ or not any(year in name for year in self.years)
69
+ or (
70
+ self.contains
71
+ and not any(re.search(x, name.lower()) for x in self.contains)
72
+ )
73
+ or (
74
+ self.not_contains
75
+ and any(re.search(x, name.lower()) for x in self.not_contains)
76
+ )
77
+ ):
78
+ continue
79
+
80
+ this_tile = {}
81
+ this_tile["name"] = name
82
+ this_tile["bbox"] = this_bbox
83
+ year = name.split(" ")[-1]
84
+ if year.isnumeric() and len(year) == 4:
85
+ this_tile["year"] = year
86
+ else:
87
+ this_tile["year"] = "9999"
88
+ all_tiles.append(this_tile)
89
+
90
+ self.tiles = sorted(all_tiles, key=lambda x: x["year"])
91
+
92
+ def get_tiles(self, bbox: Any, max_zoom: int = 40) -> list[folium.WmsTileLayer]:
93
+ """Get all Norge i bilder tiles intersecting with a bbox."""
94
+ if self.tiles is None:
95
+ self.load_tiles()
96
+
97
+ all_tiles = {}
98
+
99
+ bbox = to_shapely(bbox)
100
+
101
+ for tile in self.tiles:
102
+ if not tile["bbox"] or not tile["bbox"].intersects(bbox):
103
+ continue
104
+
105
+ name = tile["name"]
106
+
107
+ if (
108
+ not name
109
+ or not any(year in name for year in self.years)
110
+ or (
111
+ self.contains
112
+ and not any(re.search(x, name.lower()) for x in self.contains)
113
+ )
114
+ or (
115
+ self.not_contains
116
+ and any(re.search(x, name.lower()) for x in self.not_contains)
117
+ )
118
+ ):
119
+ continue
120
+
121
+ all_tiles[name] = folium.WmsTileLayer(
122
+ url="https://wms.geonorge.no/skwms1/wms.nib-prosjekter",
123
+ name=name,
124
+ layers=name,
125
+ format="image/png", # Tile format
126
+ transparent=True, # Allow transparency
127
+ version="1.3.0", # WMS version
128
+ attr="&copy; <a href='https://www.geonorge.no/'>Geonorge</a>",
129
+ show=False,
130
+ max_zoom=max_zoom,
131
+ )
132
+
133
+ return all_tiles
134
+
135
+ def __post_init__(self) -> None:
136
+ """Fix typings."""
137
+ if self.contains and isinstance(self.contains, str):
138
+ self.contains = [self.contains.lower()]
139
+ elif self.contains:
140
+ self.contains = [x.lower() for x in self.contains]
141
+
142
+ if self.not_contains and isinstance(self.not_contains, str):
143
+ self.not_contains = [self.not_contains.lower()]
144
+ elif self.not_contains:
145
+ self.not_contains = [x.lower() for x in self.not_contains]
146
+
147
+ self.years = [str(int(year)) for year in self.years]
148
+
149
+ if all(year in DEFAULT_YEARS for year in self.years):
150
+ try:
151
+ with open(JSON_PATH, encoding="utf-8") as file:
152
+ self.tiles = json.load(file)
153
+ except FileNotFoundError:
154
+ self.tiles = None
155
+ return
156
+ self.tiles = [
157
+ {
158
+ key: value if key != "bbox" else shapely.wkt.loads(value)
159
+ for key, value in tile.items()
160
+ }
161
+ for tile in self.tiles
162
+ if any(str(year) in tile["name"] for year in self.years)
163
+ ]
164
+ else:
165
+ self.tiles = None
@@ -499,6 +499,14 @@ class _ImageBase:
499
499
  "metadata": self.metadata,
500
500
  }
501
501
 
502
+ @property
503
+ def _common_init_kwargs_after_load(self) -> dict:
504
+ return {
505
+ k: v
506
+ for k, v in self._common_init_kwargs.items()
507
+ if k not in {"res", "metadata"}
508
+ }
509
+
502
510
  @property
503
511
  def path(self) -> str:
504
512
  try:
@@ -1136,8 +1144,8 @@ class Band(_ImageBandBase):
1136
1144
  if self.nodata is None or np.isnan(self.nodata):
1137
1145
  self.nodata = src.nodata
1138
1146
  else:
1139
- dtype_min_value = _get_dtype_min(src.dtypes[0])
1140
- dtype_max_value = _get_dtype_max(src.dtypes[0])
1147
+ dtype_min_value = _get_dtype_min_value(src.dtypes[0])
1148
+ dtype_max_value = _get_dtype_max_value(src.dtypes[0])
1141
1149
  if self.nodata > dtype_max_value or self.nodata < dtype_min_value:
1142
1150
  src._dtypes = tuple(
1143
1151
  rasterio.dtypes.get_minimum_dtype(self.nodata)
@@ -1240,17 +1248,17 @@ class Band(_ImageBandBase):
1240
1248
  if self.crs is None:
1241
1249
  raise ValueError("Cannot write None crs to image.")
1242
1250
 
1243
- if self.nodata:
1244
- # TODO take out .data if masked?
1245
- values_with_nodata = np.concatenate(
1246
- [self.values.flatten(), np.array([self.nodata])]
1247
- )
1248
- else:
1249
- values_with_nodata = self.values
1251
+ try:
1252
+ data = self.values.data
1253
+ except AttributeError:
1254
+ data = self.values
1255
+ data = np.array([np.min(data), np.max(data), self.nodata or 0])
1256
+ min_dtype = rasterio.dtypes.get_minimum_dtype(data)
1257
+
1250
1258
  profile = {
1251
1259
  "driver": driver,
1252
1260
  "compress": compress,
1253
- "dtype": rasterio.dtypes.get_minimum_dtype(values_with_nodata),
1261
+ "dtype": min_dtype,
1254
1262
  "crs": self.crs,
1255
1263
  "transform": self.transform,
1256
1264
  "nodata": self.nodata,
@@ -1263,7 +1271,7 @@ class Band(_ImageBandBase):
1263
1271
  with rasterio.open(f, "w", **profile) as dst:
1264
1272
 
1265
1273
  if dst.nodata is None:
1266
- dst.nodata = _get_dtype_min(dst.dtypes[0])
1274
+ dst.nodata = _get_dtype_min_value(dst.dtypes[0])
1267
1275
 
1268
1276
  if (
1269
1277
  isinstance(self.values, np.ma.core.MaskedArray)
@@ -1515,12 +1523,6 @@ class NDVIBand(Band):
1515
1523
  cmap: str = "Greens"
1516
1524
 
1517
1525
 
1518
- def median_as_int_and_minimum_dtype(arr: np.ndarray) -> np.ndarray:
1519
- arr = np.median(arr, axis=0).astype(int)
1520
- min_dtype = rasterio.dtypes.get_minimum_dtype(arr)
1521
- return arr.astype(min_dtype)
1522
-
1523
-
1524
1526
  class Image(_ImageBandBase):
1525
1527
  """Image consisting of one or more Bands."""
1526
1528
 
@@ -1742,7 +1744,7 @@ class Image(_ImageBandBase):
1742
1744
  arr,
1743
1745
  bounds=red.bounds,
1744
1746
  crs=red.crs,
1745
- **{k: v for k, v in red._common_init_kwargs.items() if k != "res"},
1747
+ **red._common_init_kwargs_after_load,
1746
1748
  )
1747
1749
 
1748
1750
  def get_brightness(
@@ -1773,7 +1775,7 @@ class Image(_ImageBandBase):
1773
1775
  brightness,
1774
1776
  bounds=red.bounds,
1775
1777
  crs=self.crs,
1776
- **{k: v for k, v in self._common_init_kwargs.items() if k != "res"},
1778
+ **self._common_init_kwargs_after_load,
1777
1779
  )
1778
1780
 
1779
1781
  def to_xarray(self) -> DataArray:
@@ -2102,7 +2104,7 @@ class ImageCollection(_ImageBase):
2102
2104
 
2103
2105
  for attr in by:
2104
2106
  if attr == "bounds":
2105
- # need integers to check equality when grouping
2107
+ # need integers to properly check equality when grouping
2106
2108
  df[attr] = [
2107
2109
  tuple(int(x) for x in band.bounds) for img in self for band in img
2108
2110
  ]
@@ -2322,9 +2324,8 @@ class ImageCollection(_ImageBase):
2322
2324
  arr,
2323
2325
  bounds=bounds,
2324
2326
  crs=crs,
2325
- **{k: v for k, v in self._common_init_kwargs.items() if k != "res"},
2327
+ **self._common_init_kwargs_after_load,
2326
2328
  )
2327
-
2328
2329
  band._merged = True
2329
2330
  return band
2330
2331
 
@@ -2390,19 +2391,20 @@ class ImageCollection(_ImageBase):
2390
2391
 
2391
2392
  arrs.append(arr)
2392
2393
  bands.append(
2393
- self.band_class(
2394
+ # self.band_class(
2395
+ Band(
2394
2396
  arr,
2395
2397
  bounds=out_bounds,
2396
2398
  crs=crs,
2397
2399
  band_id=band_id,
2398
- **{k: v for k, v in self._common_init_kwargs.items() if k != "res"},
2400
+ **self._common_init_kwargs_after_load,
2399
2401
  )
2400
2402
  )
2401
2403
 
2402
2404
  # return self.image_class( # TODO
2403
2405
  image = Image(
2404
2406
  bands,
2405
- band_class=self.band_class,
2407
+ # band_class=self.band_class,
2406
2408
  **self._common_init_kwargs,
2407
2409
  )
2408
2410
 
@@ -2431,25 +2433,17 @@ class ImageCollection(_ImageBase):
2431
2433
  continue
2432
2434
 
2433
2435
  _bounds = to_bbox(_bounds)
2434
- arr = np.array(
2435
- [
2436
- (
2437
- # band.load(
2438
- # bounds=(_bounds if _bounds is not None else None),
2439
- # **kwargs,
2440
- # )
2441
- # if not band.has_array
2442
- # else
2443
- band
2444
- ).values
2445
- for img in collection
2446
- for band in img
2447
- ]
2448
- )
2436
+ collection.load(bounds=(_bounds if _bounds is not None else None), **kwargs)
2437
+ arr = np.array([band.values for img in collection for band in img])
2449
2438
  arr = numpy_func(arr, axis=0)
2450
2439
  if as_int:
2451
2440
  arr = arr.astype(int)
2452
- min_dtype = rasterio.dtypes.get_minimum_dtype(arr)
2441
+ try:
2442
+ data = arr.data
2443
+ except AttributeError:
2444
+ data = arr
2445
+ data = np.array([np.min(data), np.max(data), self.nodata or 0])
2446
+ min_dtype = rasterio.dtypes.get_minimum_dtype(data)
2453
2447
  arr = arr.astype(min_dtype)
2454
2448
 
2455
2449
  if len(arr.shape) == 2:
@@ -3429,24 +3423,20 @@ def _date_is_within(
3429
3423
  return False
3430
3424
 
3431
3425
 
3432
- def _get_dtype_min(dtype: str | type) -> int | float:
3426
+ def _get_dtype_min_value(dtype: str | type) -> int | float:
3433
3427
  try:
3434
3428
  return np.iinfo(dtype).min
3435
3429
  except ValueError:
3436
3430
  return np.finfo(dtype).min
3437
3431
 
3438
3432
 
3439
- def _get_dtype_max(dtype: str | type) -> int | float:
3433
+ def _get_dtype_max_value(dtype: str | type) -> int | float:
3440
3434
  try:
3441
3435
  return np.iinfo(dtype).max
3442
3436
  except ValueError:
3443
3437
  return np.finfo(dtype).max
3444
3438
 
3445
3439
 
3446
- def _intesects(x, other) -> bool:
3447
- return box(*x.bounds).intersects(other)
3448
-
3449
-
3450
3440
  def _copy_and_add_df_parallel(
3451
3441
  group_values: tuple[Any, ...],
3452
3442
  group_df: pd.DataFrame,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ssb-sgis
3
- Version: 1.0.12
3
+ Version: 1.0.14
4
4
  Summary: GIS functions used at Statistics Norway.
5
5
  Home-page: https://github.com/statisticsnorway/ssb-sgis
6
6
  License: MIT
@@ -1,4 +1,4 @@
1
- sgis/__init__.py,sha256=kqHf60QUo0_TaB1ikBQXkxCQK4hUjSM6u_GKfLJ3C-4,7351
1
+ sgis/__init__.py,sha256=h_5A40stVg0dKwoQRDmv_owy-x-n66pDpM7XtcHMSwE,7404
2
2
  sgis/debug_config.py,sha256=Tfr19kU46hSkkspsIJcrUWvlhaL4U3-f8xEPkujSCAQ,593
3
3
  sgis/exceptions.py,sha256=WNaEBPNNx0rmz-YDzlFX4vIE7ocJQruUTqS2RNAu2zU,660
4
4
  sgis/geopandas_tools/__init__.py,sha256=bo8lFMcltOz7TtWAi52_ekR2gd3mjfBfKeMDV5zuqFY,28
@@ -24,11 +24,13 @@ sgis/io/opener.py,sha256=HWO3G1NB6bpXKM94JadCD513vjat1o1TFjWGWzyVasg,898
24
24
  sgis/io/read_parquet.py,sha256=FvZYv1rLkUlrSaUY6QW6E1yntmntTeQuZ9ZRgCDO4IM,3776
25
25
  sgis/maps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  sgis/maps/examine.py,sha256=Pb0dH8JazU5E2svfQrzHO1Bi-sjy5SeyY6zoeMO34jE,9369
27
- sgis/maps/explore.py,sha256=QYPnpiLmCwMIduytNJ9K9xMUGPmb76i3ONfAH_WeNO0,45782
27
+ sgis/maps/explore.py,sha256=LO5ESH4e5M_fXIZzvnd-lkGyYmWcARcJYhpcZX6fLsg,47301
28
28
  sgis/maps/httpserver.py,sha256=7Od9JMCtntcIQKk_TchetojMHzFHT9sPw7GANahI97c,1982
29
29
  sgis/maps/legend.py,sha256=lVRVCkhPmJRjGK23obFJZAO3qp6du1LYnobkkN7DPkc,26279
30
30
  sgis/maps/map.py,sha256=smaf9i53EoRZWmZjn9UuqlhzUvVs1XKo2ItIpHxyuik,29592
31
- sgis/maps/maps.py,sha256=9uidfWhbdJzO6lzDUjn4EWJMpCtc4st3uy4dRbNRVtQ,22430
31
+ sgis/maps/maps.py,sha256=vOB09wiquW7-wGEqHJMotFOBX8tFfnD4gcvvpYf5Wfo,23599
32
+ sgis/maps/norge_i_bilder.json,sha256=W_mFfte3DxugWbEudZ5fadZ2JeFYb0hyab2Quf4oJME,481311
33
+ sgis/maps/norge_i_bilder_wms.py,sha256=Pb1puQFCZfEO_ng915_aOkB17wpZLbRMnUEBAiLPzjQ,5698
32
34
  sgis/maps/thematicmap.py,sha256=yAE1xEfubJcDmBlOJf-Q3SVae1ZHIEMP-YB95Wy8cRw,21691
33
35
  sgis/maps/tilesources.py,sha256=F4mFHxPwkiPJdVKzNkScTX6xbJAMIUtlTq4mQ83oguw,1746
34
36
  sgis/networkanalysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -49,12 +51,12 @@ sgis/parallel/parallel.py,sha256=SlC_mOwvSSyWTKUcxLMGkuWHUkEC6dXTlN0Jn5cAtxA,396
49
51
  sgis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
52
  sgis/raster/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
53
  sgis/raster/base.py,sha256=tiZEuMcVK6hOm_aIjWhQ1WGshcjsxT1fFkuBSLFiMC0,7785
52
- sgis/raster/image_collection.py,sha256=C_w_18vOP1Z7-EHzGYJSYDwnWA7tg5OlRzGlLJam0pc,122878
54
+ sgis/raster/image_collection.py,sha256=STPh8m2WRAkuQfuJC1VNA2XLZ6FHT8I7qjcAntVWi2o,122573
53
55
  sgis/raster/indices.py,sha256=-J1HYmnT240iozvgagvyis6K0_GHZHRuUrPOgyoeIrY,223
54
56
  sgis/raster/regex.py,sha256=kYhVpRYzoXutx1dSYmqMoselWXww7MMEsTPmLZwHjbM,3759
55
57
  sgis/raster/sentinel_config.py,sha256=nySDqn2R8M6W8jguoBeSAK_zzbAsqmaI59i32446FwY,1268
56
58
  sgis/raster/zonal.py,sha256=D4Gyptw-yOLTCO41peIuYbY-DANsJCG19xXDlf1QAz4,2299
57
- ssb_sgis-1.0.12.dist-info/LICENSE,sha256=np3IfD5m0ZUofn_kVzDZqliozuiO6wrktw3LRPjyEiI,1073
58
- ssb_sgis-1.0.12.dist-info/METADATA,sha256=Ys8MxFw0PN2sbDAQFQiYsS5wNIFlV02Qb9n90Iegqvs,11741
59
- ssb_sgis-1.0.12.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
60
- ssb_sgis-1.0.12.dist-info/RECORD,,
59
+ ssb_sgis-1.0.14.dist-info/LICENSE,sha256=np3IfD5m0ZUofn_kVzDZqliozuiO6wrktw3LRPjyEiI,1073
60
+ ssb_sgis-1.0.14.dist-info/METADATA,sha256=fey5-NS1CXLcspl94MR5TNZeGIXjwbQmefO3UCTYkJI,11741
61
+ ssb_sgis-1.0.14.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
62
+ ssb_sgis-1.0.14.dist-info/RECORD,,