ssb-sgis 1.2.7__py3-none-any.whl → 1.2.9__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.
sgis/maps/wms.py CHANGED
@@ -4,18 +4,33 @@ import json
4
4
  import re
5
5
  from collections.abc import Iterable
6
6
  from dataclasses import dataclass
7
+ from io import BytesIO
7
8
  from pathlib import Path
8
9
  from typing import Any
9
10
  from urllib.request import urlopen
10
11
 
11
12
  import folium
13
+ import numpy as np
14
+ import pandas as pd
12
15
  import shapely
13
-
16
+ from geopandas import GeoDataFrame
17
+ from geopandas import GeoSeries
18
+ from shapely import Geometry
19
+ from shapely import get_exterior_ring
20
+ from shapely import make_valid
21
+ from shapely import polygons
22
+ from shapely import set_precision
23
+ from shapely import simplify
24
+ from shapely.errors import GEOSException
25
+
26
+ from ..geopandas_tools.conversion import to_gdf
14
27
  from ..geopandas_tools.conversion import to_shapely
28
+ from ..geopandas_tools.sfilter import sfilter
29
+ from ..raster.image_collection import Band
15
30
 
16
31
  JSON_PATH = Path(__file__).parent / "norge_i_bilder.json"
17
32
 
18
- JSON_YEARS = [str(year) for year in range(1999, 2025)]
33
+ JSON_YEARS = [str(year) for year in range(2006, datetime.datetime.now().year + 1)]
19
34
 
20
35
  DEFAULT_YEARS: tuple[str] = tuple(
21
36
  str(year)
@@ -57,7 +72,7 @@ class NorgeIBilderWms(WmsLoader):
57
72
  show: bool | Iterable[int] | int = False
58
73
  _use_json: bool = True
59
74
 
60
- def load_tiles(self) -> None:
75
+ def load_tiles(self, verbose: bool = False) -> None:
61
76
  """Load all Norge i bilder tiles into self.tiles."""
62
77
  url = "https://wms.geonorge.no/skwms1/wms.nib-prosjekter?SERVICE=WMS&REQUEST=GetCapabilities"
63
78
 
@@ -107,50 +122,135 @@ class NorgeIBilderWms(WmsLoader):
107
122
  this_tile["name"] = name
108
123
  this_tile["bbox"] = this_bbox
109
124
  year = name.split(" ")[-1]
110
- if year.isnumeric() and len(year) == 4:
125
+ is_year_or_interval: bool = all(
126
+ part.isnumeric() and len(part) == 4 for part in year.split("-")
127
+ )
128
+ if is_year_or_interval:
111
129
  this_tile["year"] = year
112
130
  else:
113
131
  this_tile["year"] = "9999"
114
132
 
115
133
  all_tiles.append(this_tile)
116
134
 
117
- self.tiles = sorted(all_tiles, key=lambda x: x["year"])
135
+ self.tiles = sorted(all_tiles, key=lambda x: (x["year"]))
118
136
 
119
- def get_tiles(self, bbox: Any, max_zoom: int = 40) -> list[folium.WmsTileLayer]:
120
- """Get all Norge i bilder tiles intersecting with a bbox."""
137
+ masks = self._get_norge_i_bilder_polygon_masks(verbose=verbose)
138
+ for tile in self.tiles:
139
+ mask = masks.get(tile["name"], None)
140
+ tile["geometry"] = mask
141
+
142
+ def _get_norge_i_bilder_polygon_masks(self, verbose: bool):
143
+ from owslib.util import ServiceException
144
+ from owslib.wms import WebMapService
145
+ from PIL import Image
146
+
147
+ relevant_names: dict[str, str] = {x["name"]: x["bbox"] for x in self.tiles}
148
+ assert len(relevant_names), relevant_names
149
+
150
+ url = "https://wms.geonorge.no/skwms1/wms.nib-mosaikk?SERVICE=WMS&REQUEST=GetCapabilities"
151
+ wms = WebMapService(url, version="1.3.0")
152
+ out = {}
153
+ # ttiles = {wms[layer].title: [] for layer in list(wms.contents)}
154
+ # for layer in list(wms.contents):
155
+ # if wms[layer].title not in relevant_names:
156
+ # continue
157
+ # ttiles[wms[layer].title].append(layer)
158
+ # import pandas as pd
159
+
160
+ # df = pd.Series(ttiles).to_frame("title")
161
+ # df["n"] = df["title"].str.len()
162
+ # df = df.sort_values("n")
163
+ # for x in df["title"]:
164
+ # if len(x) == 1:
165
+ # continue
166
+ # bounds = {tuple(wms[layer].boundingBoxWGS84) for layer in x}
167
+ # if len(bounds) <= 1:
168
+ # continue
169
+ # print()
170
+ # for layer in x:
171
+ # print(layer)
172
+ # print(wms[layer].title)
173
+ # bbox = wms[layer].boundingBoxWGS84
174
+ # print(bbox)
175
+
176
+ for layer in list(wms.contents):
177
+ title = wms[layer].title
178
+ if title not in relevant_names:
179
+ continue
180
+ bbox = wms[layer].boundingBoxWGS84
181
+ bbox = tuple(to_gdf(bbox, crs=4326).to_crs(25832).total_bounds)
182
+
183
+ existing_bbox = relevant_names[title]
184
+ existing_bbox = to_gdf(existing_bbox, crs=4326).to_crs(25832).union_all()
185
+ if not to_shapely(bbox).intersects(existing_bbox):
186
+ continue
187
+ diffx = bbox[2] - bbox[0]
188
+ diffy = bbox[3] - bbox[1]
189
+ width = int(diffx / 40)
190
+ height = int(diffy / 40)
191
+ if not bbox:
192
+ continue
193
+ try:
194
+ img = wms.getmap(
195
+ layers=[layer],
196
+ styles=[""], # Empty unless you know the style
197
+ srs="EPSG:25832",
198
+ bbox=bbox,
199
+ size=(width, height),
200
+ format="image/jpeg",
201
+ transparent=True,
202
+ bgcolor="#FFFFFF",
203
+ )
204
+ except (ServiceException, AttributeError) as e:
205
+ if verbose:
206
+ print(type(e), e)
207
+ continue
208
+
209
+ arr = np.array(Image.open(BytesIO(img.read())))
210
+ if not np.sum(arr):
211
+ continue
212
+
213
+ band = Band(
214
+ np.where(np.any(arr != 0, axis=-1), 1, 0), bounds=bbox, crs=25832
215
+ )
216
+ polygon = band.to_geopandas()[lambda x: x["value"] == 1].geometry.values
217
+ polygon = make_valid(polygons(get_exterior_ring(polygon)))
218
+ polygon = make_valid(set_precision(polygon, 1))
219
+ polygon = make_valid(simplify(polygon, 100))
220
+ polygon = make_valid(set_precision(polygon, 1))
221
+ polygon = GeoSeries(polygon, crs=25832).to_crs(4326)
222
+ if verbose:
223
+ print(f"Layer name: {layer}")
224
+ print(f"Title: {wms[layer].title}")
225
+ print(f"Bounding box: {wms[layer].boundingBoxWGS84}")
226
+ print(f"polygon: {polygon}")
227
+ print("-" * 40)
228
+
229
+ for x in [0, 0.1, 0.001, 1]:
230
+ try:
231
+ out[title] = make_valid(polygon.buffer(x).make_valid().union_all())
232
+ except GEOSException:
233
+ pass
234
+ break
235
+
236
+ return out
237
+
238
+ def get_tiles(self, mask: Any, max_zoom: int = 40) -> list[folium.WmsTileLayer]:
239
+ """Get all Norge i bilder tiles intersecting with a mask (bbox or polygon)."""
121
240
  if self.tiles is None:
122
241
  self.load_tiles()
123
242
 
124
- all_tiles = {}
125
-
126
- bbox = to_shapely(bbox)
243
+ if not isinstance(mask, (GeoSeries | GeoDataFrame | Geometry)):
244
+ mask = to_shapely(mask)
127
245
 
128
246
  if isinstance(self.show, bool):
129
247
  show = self.show
130
248
  else:
131
249
  show = False
132
250
 
133
- for tile in self.tiles:
134
- if not tile["bbox"] or not tile["bbox"].intersects(bbox):
135
- continue
136
-
137
- name = tile["name"]
138
-
139
- if (
140
- not name
141
- or not any(year in name for year in self.years)
142
- or (
143
- self.contains
144
- and not any(re.search(x, name.lower()) for x in self.contains)
145
- )
146
- or (
147
- self.not_contains
148
- and any(re.search(x, name.lower()) for x in self.not_contains)
149
- )
150
- ):
151
- continue
152
-
153
- all_tiles[name] = folium.WmsTileLayer(
251
+ relevant_tiles = self._filter_tiles(mask)
252
+ tile_layers = {
253
+ name: folium.WmsTileLayer(
154
254
  url="https://wms.geonorge.no/skwms1/wms.nib-prosjekter",
155
255
  name=name,
156
256
  layers=name,
@@ -161,28 +261,48 @@ class NorgeIBilderWms(WmsLoader):
161
261
  show=show,
162
262
  max_zoom=max_zoom,
163
263
  )
264
+ for name in relevant_tiles["name"]
265
+ }
266
+
267
+ if not len(tile_layers):
268
+ return tile_layers
164
269
 
165
270
  if isinstance(self.show, int):
166
- tile = all_tiles[list(all_tiles)[self.show]]
271
+ tile = tile_layers[list(tile_layers)[self.show]]
167
272
  tile.show = True
168
273
  elif isinstance(self.show, Iterable):
169
274
  for i in self.show:
170
- tile = all_tiles[list(all_tiles)[i]]
275
+ tile = tile_layers[list(tile_layers)[i]]
171
276
  tile.show = True
172
277
 
173
- return all_tiles
278
+ return tile_layers
279
+
280
+ def _filter_tiles(self, mask):
281
+ """Filter relevant dates with pandas and geopandas because fast."""
282
+ df = pd.DataFrame(self.tiles)
283
+ filt = (df["name"].notna()) & (df["year"].str.contains("|".join(self.years)))
284
+ if self.contains:
285
+ for x in self.contains:
286
+ filt &= df["name"].str.contains(x)
287
+ if self.not_contains:
288
+ for x in self.not_contains:
289
+ filt &= ~df["name"].str.contains(x)
290
+ df = df[filt]
291
+ geoms = np.where(df["geometry"].notna(), df["geometry"], df["bbox"])
292
+ geoms = GeoSeries(geoms)
293
+ assert geoms.index.is_unique
294
+ return df.iloc[sfilter(geoms, mask).index]
174
295
 
175
296
  def __post_init__(self) -> None:
176
297
  """Fix typings."""
177
298
  if self.contains and isinstance(self.contains, str):
178
- self.contains = [self.contains.lower()]
299
+ self.contains = [self.contains]
179
300
  elif self.contains:
180
- self.contains = [x.lower() for x in self.contains]
181
-
301
+ self.contains = [x for x in self.contains]
182
302
  if self.not_contains and isinstance(self.not_contains, str):
183
- self.not_contains = [self.not_contains.lower()]
303
+ self.not_contains = [self.not_contains]
184
304
  elif self.not_contains:
185
- self.not_contains = [x.lower() for x in self.not_contains]
305
+ self.not_contains = [x for x in self.not_contains]
186
306
 
187
307
  self.years = [str(int(year)) for year in self.years]
188
308
 
@@ -195,7 +315,11 @@ class NorgeIBilderWms(WmsLoader):
195
315
  return
196
316
  self.tiles = [
197
317
  {
198
- key: value if key != "bbox" else shapely.wkt.loads(value)
318
+ key: (
319
+ value
320
+ if key not in ["bbox", "geometry"]
321
+ else shapely.wkt.loads(value)
322
+ )
199
323
  for key, value in tile.items()
200
324
  }
201
325
  for tile in self.tiles
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ssb-sgis
3
- Version: 1.2.7
3
+ Version: 1.2.9
4
4
  Summary: GIS functions used at Statistics Norway.
5
5
  Home-page: https://github.com/statisticsnorway/ssb-sgis
6
6
  License: MIT
@@ -4,7 +4,7 @@ sgis/debug_config.py,sha256=Tfr19kU46hSkkspsIJcrUWvlhaL4U3-f8xEPkujSCAQ,593
4
4
  sgis/exceptions.py,sha256=WNaEBPNNx0rmz-YDzlFX4vIE7ocJQruUTqS2RNAu2zU,660
5
5
  sgis/geopandas_tools/__init__.py,sha256=bo8lFMcltOz7TtWAi52_ekR2gd3mjfBfKeMDV5zuqFY,28
6
6
  sgis/geopandas_tools/bounds.py,sha256=YJyF0gp78hFAjLLZmDquRKCBAtbt7QouG3snTcJeNQs,23822
7
- sgis/geopandas_tools/buffer_dissolve_explode.py,sha256=ReIgoeh6CUVcLmsUZ_pyoWYg6iBZzYiGmFq6CMOKRvE,19535
7
+ sgis/geopandas_tools/buffer_dissolve_explode.py,sha256=z9HvakazR_prXH862e8-gEe7UFbeI4rRTbUaBgPeMBk,19552
8
8
  sgis/geopandas_tools/centerlines.py,sha256=Q65Sx01SeAlulBEd9oaZkB2maBBNdLcJwAbTILg4SPU,11848
9
9
  sgis/geopandas_tools/cleaning.py,sha256=fST0xFztmyn-QUOAfvjZmu7aO_zPiolWK7gd7TR6ffI,24393
10
10
  sgis/geopandas_tools/conversion.py,sha256=CrasgWHAnUmLC5tP73ZTDjQ6ahKFHQGqWj86PUif24M,24176
@@ -28,15 +28,15 @@ sgis/io/opener.py,sha256=HWO3G1NB6bpXKM94JadCD513vjat1o1TFjWGWzyVasg,898
28
28
  sgis/io/read_parquet.py,sha256=FvZYv1rLkUlrSaUY6QW6E1yntmntTeQuZ9ZRgCDO4IM,3776
29
29
  sgis/maps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  sgis/maps/examine.py,sha256=Pb0dH8JazU5E2svfQrzHO1Bi-sjy5SeyY6zoeMO34jE,9369
31
- sgis/maps/explore.py,sha256=1kdxHpEPlzgkvsYRoqkJxh0sKyjQLualWy-PhHnEwl4,47812
31
+ sgis/maps/explore.py,sha256=azfNNcEEiNp2lJxWN1Emf7kcmG60WL_ok_ilMw0-IL8,47914
32
32
  sgis/maps/httpserver.py,sha256=TETSGOgLjKl3TquPGoIP0tCJCz7BIwmXrrzSCT7jhXE,2550
33
33
  sgis/maps/legend.py,sha256=qq2RkebuaNAdFztlXrDOWbN0voeK5w5VycmRKyx0NdM,26512
34
34
  sgis/maps/map.py,sha256=XWf3QJ6a4gZno2NziK1dKLRktJGGr-vn6eHudBlW9Uc,30758
35
- sgis/maps/maps.py,sha256=66xCarrpvlgsts3ut-SnbIiexEwyy_AIed4O4tysUig,23139
36
- sgis/maps/norge_i_bilder.json,sha256=VKmb7rg4jvgc8_Ve1fFnHyZ_Dkv4T5GTA0UCpqpFAi4,492751
35
+ sgis/maps/maps.py,sha256=fLK5WUlQ2YTm7t-8260lYxCFvpZN6j0Y-bVYCyv8NAY,23249
36
+ sgis/maps/norge_i_bilder.json,sha256=G9DIN_2vyn-18UF5wUC-koZxFCbiNxMu0BbCJhMFJUk,15050340
37
37
  sgis/maps/thematicmap.py,sha256=Z3o_Bca0oty5Cn35pZfX5Qy52sXDVIMVSFD6IlZrovo,25111
38
38
  sgis/maps/tilesources.py,sha256=F4mFHxPwkiPJdVKzNkScTX6xbJAMIUtlTq4mQ83oguw,1746
39
- sgis/maps/wms.py,sha256=LSBtKkG5d-sggK_qO8BvOHBvPZZV_7AAo25kHF6kPio,6933
39
+ sgis/maps/wms.py,sha256=jQGF7na-IruG7FF3nHzvZB0BkdkhiWRRwfhkkx4aWkU,11970
40
40
  sgis/networkanalysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  sgis/networkanalysis/_get_route.py,sha256=dMX4Vm6O90ISIZPjQWuZMVMuEubkeSdC2osMCbFvrRU,7750
42
42
  sgis/networkanalysis/_od_cost_matrix.py,sha256=zkyPX7ObT996ahaFJ2oI0D0SqQWbWyfy_qLtXwValPg,3434
@@ -61,7 +61,7 @@ sgis/raster/indices.py,sha256=efJmgfPg_VuSzXFosXV661IendF8CwPFWtMhyP4TMUg,222
61
61
  sgis/raster/regex.py,sha256=4idTJ9vFtsGtbxcjJrx2VrpJJuDMP3bLdqF93Vc_cmY,3752
62
62
  sgis/raster/sentinel_config.py,sha256=nySDqn2R8M6W8jguoBeSAK_zzbAsqmaI59i32446FwY,1268
63
63
  sgis/raster/zonal.py,sha256=D4Gyptw-yOLTCO41peIuYbY-DANsJCG19xXDlf1QAz4,2299
64
- ssb_sgis-1.2.7.dist-info/LICENSE,sha256=np3IfD5m0ZUofn_kVzDZqliozuiO6wrktw3LRPjyEiI,1073
65
- ssb_sgis-1.2.7.dist-info/METADATA,sha256=oGAi8NDi4GwCfUxWBbJVgRDROiMrb_xrqf13vwhc-n0,11740
66
- ssb_sgis-1.2.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
67
- ssb_sgis-1.2.7.dist-info/RECORD,,
64
+ ssb_sgis-1.2.9.dist-info/LICENSE,sha256=np3IfD5m0ZUofn_kVzDZqliozuiO6wrktw3LRPjyEiI,1073
65
+ ssb_sgis-1.2.9.dist-info/METADATA,sha256=yFVJtJkPtMRAxhQc10C0urtlsr6SNkjQkaBeFlCdZ88,11740
66
+ ssb_sgis-1.2.9.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
67
+ ssb_sgis-1.2.9.dist-info/RECORD,,