ssb-sgis 1.2.17__tar.gz → 1.3.0__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 (67) hide show
  1. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/PKG-INFO +1 -1
  2. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/pyproject.toml +1 -1
  3. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/general.py +5 -1
  4. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/io/dapla_functions.py +5 -5
  5. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/wms.py +115 -69
  6. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/LICENSE +0 -0
  7. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/README.md +0 -0
  8. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/__init__.py +0 -0
  9. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/conf.py +0 -0
  10. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/debug_config.py +0 -0
  11. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/exceptions.py +0 -0
  12. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/__init__.py +0 -0
  13. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/bounds.py +0 -0
  14. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/buffer_dissolve_explode.py +0 -0
  15. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/centerlines.py +0 -0
  16. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/cleaning.py +0 -0
  17. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/conversion.py +0 -0
  18. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/duplicates.py +0 -0
  19. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/geocoding.py +0 -0
  20. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/geometry_types.py +0 -0
  21. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/neighbors.py +0 -0
  22. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/overlay.py +0 -0
  23. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/point_operations.py +0 -0
  24. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/polygon_operations.py +0 -0
  25. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/polygons_as_rings.py +0 -0
  26. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/runners.py +0 -0
  27. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/sfilter.py +0 -0
  28. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/geopandas_tools/utils.py +0 -0
  29. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/helpers.py +0 -0
  30. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/io/__init__.py +0 -0
  31. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/io/_is_dapla.py +0 -0
  32. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/io/opener.py +0 -0
  33. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/io/read_parquet.py +0 -0
  34. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/__init__.py +0 -0
  35. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/examine.py +0 -0
  36. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/explore.py +0 -0
  37. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/httpserver.py +0 -0
  38. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/legend.py +0 -0
  39. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/map.py +0 -0
  40. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/maps.py +0 -0
  41. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/norge_i_bilder.json +0 -0
  42. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/thematicmap.py +0 -0
  43. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/maps/tilesources.py +0 -0
  44. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/__init__.py +0 -0
  45. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/_get_route.py +0 -0
  46. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/_od_cost_matrix.py +0 -0
  47. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/_points.py +0 -0
  48. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/_service_area.py +0 -0
  49. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/closing_network_holes.py +0 -0
  50. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/cutting_lines.py +0 -0
  51. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/directednetwork.py +0 -0
  52. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/finding_isolated_networks.py +0 -0
  53. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/network.py +0 -0
  54. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/networkanalysis.py +0 -0
  55. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/networkanalysisrules.py +0 -0
  56. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/nodes.py +0 -0
  57. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/networkanalysis/traveling_salesman.py +0 -0
  58. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/parallel/__init__.py +0 -0
  59. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/parallel/parallel.py +0 -0
  60. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/py.typed +0 -0
  61. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/raster/__init__.py +0 -0
  62. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/raster/base.py +0 -0
  63. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/raster/image_collection.py +0 -0
  64. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/raster/indices.py +0 -0
  65. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/raster/regex.py +0 -0
  66. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/raster/sentinel_config.py +0 -0
  67. {ssb_sgis-1.2.17 → ssb_sgis-1.3.0}/src/sgis/raster/zonal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ssb-sgis
3
- Version: 1.2.17
3
+ Version: 1.3.0
4
4
  Summary: GIS functions used at Statistics Norway.
5
5
  Home-page: https://github.com/statisticsnorway/ssb-sgis
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ssb-sgis"
3
- version = "1.2.17"
3
+ version = "1.3.0"
4
4
  description = "GIS functions used at Statistics Norway."
5
5
  authors = ["Morten Letnes <morten.letnes@ssb.no>"]
6
6
  license = "MIT"
@@ -21,6 +21,7 @@ from shapely import get_coordinates
21
21
  from shapely import get_parts
22
22
  from shapely import linestrings
23
23
  from shapely import make_valid
24
+ from shapely.errors import GEOSException
24
25
  from shapely.geometry import LineString
25
26
  from shapely.geometry import MultiPoint
26
27
  from shapely.geometry import Point
@@ -211,7 +212,10 @@ def clean_geoms(
211
212
  if isinstance(gdf, GeoDataFrame):
212
213
  # only repair if necessary
213
214
  if not gdf.geometry.is_valid.all():
214
- gdf.geometry = gdf.make_valid()
215
+ try:
216
+ gdf.geometry = gdf.make_valid()
217
+ except GEOSException as e:
218
+ raise type(e)(f"{e}: {gdf.geometry}") from e
215
219
 
216
220
  notna = gdf.geometry.notna()
217
221
  if not notna.all():
@@ -464,7 +464,7 @@ def write_geopandas(
464
464
  )
465
465
 
466
466
  if not len(df) and get_child_paths(gcs_path, file_system):
467
- # no need to write empty df for partitioned parquet
467
+ # no need to write empty df for partitioned parquet - if root dir exists
468
468
  return
469
469
  elif not len(df):
470
470
  if pandas_fallback:
@@ -586,9 +586,6 @@ def _write_partitioned_geoparquet(
586
586
  ):
587
587
  file_system = _get_file_system(file_system, kwargs)
588
588
 
589
- if basename_template is None:
590
- basename_template = uuid.uuid4().hex + "-{i}.parquet"
591
-
592
589
  if isinstance(partition_cols, str):
593
590
  partition_cols = [partition_cols]
594
591
 
@@ -623,7 +620,10 @@ def _write_partitioned_geoparquet(
623
620
  dfs.append(rows)
624
621
 
625
622
  def threaded_write(rows: DataFrame, path: str) -> None:
626
- this_basename = basename_template.replace("-{i}", "0")
623
+ if basename_template is None:
624
+ this_basename = (uuid.uuid4().hex + "-{i}.parquet").replace("-{i}", "0")
625
+ else:
626
+ this_basename = basename_template
627
627
  for i, sibling_path in enumerate(sorted(glob_func(str(Path(path) / "**")))):
628
628
  if paths_are_equal(sibling_path, path):
629
629
  continue
@@ -3,7 +3,6 @@ import datetime
3
3
  import json
4
4
  import re
5
5
  from collections.abc import Iterable
6
- from dataclasses import dataclass
7
6
  from io import BytesIO
8
7
  from pathlib import Path
9
8
  from typing import Any
@@ -30,18 +29,17 @@ from ..raster.image_collection import Band
30
29
 
31
30
  JSON_PATH = Path(__file__).parent / "norge_i_bilder.json"
32
31
 
33
- JSON_YEARS = [str(year) for year in range(2006, datetime.datetime.now().year + 1)]
32
+ # JSON_YEARS = tuple(range(1900, datetime.datetime.now().year + 1))
33
+ JSON_YEARS = tuple(range(2006, datetime.datetime.now().year + 1))
34
34
 
35
- DEFAULT_YEARS: tuple[str] = tuple(
36
- str(year)
37
- for year in range(
35
+ DEFAULT_YEARS: tuple[int] = tuple(
36
+ range(
38
37
  int(datetime.datetime.now().year) - 10,
39
38
  int(datetime.datetime.now().year) + 1,
40
39
  )
41
40
  )
42
41
 
43
42
 
44
- @dataclass
45
43
  class WmsLoader(abc.ABC):
46
44
  """Abstract base class for wms loaders.
47
45
 
@@ -49,32 +47,82 @@ class WmsLoader(abc.ABC):
49
47
  which should return a list of folium.WmsTileLayer.
50
48
  """
51
49
 
50
+ _min_year: int = 1900
51
+
52
+ @abc.abstractmethod
53
+ def filter_tiles(
54
+ self, mask: GeoDataFrame | GeoSeries | Geometry | tuple[float]
55
+ ) -> list[str]:
56
+ """Filter relevant dates with pandas and geopandas because fast."""
57
+
52
58
  @abc.abstractmethod
53
59
  def get_tiles(self, bbox: Any, max_zoom: int = 40) -> list[folium.WmsTileLayer]:
54
60
  """Get all tiles intersecting with a bbox."""
55
61
 
56
62
  @abc.abstractmethod
57
63
  def load_tiles(self) -> None:
58
- """Load all tiles into self.tiles.
64
+ """Load all tiles into self._tiles.
59
65
 
60
66
  Not needed in sgis.explore.
61
67
  """
62
68
  pass
63
69
 
70
+ def __repr__(self) -> str:
71
+ """Print representation."""
72
+ return str(self)
73
+
74
+ def __str__(self) -> str:
75
+ """String representation."""
76
+
77
+ def maybe_to_string(value: Any):
78
+ if isinstance(value, str):
79
+ return f"'{value}'"
80
+ return value
81
+
82
+ txt = ", ".join(
83
+ f"{k}={maybe_to_string(v)}"
84
+ for k, v in self.__dict__.items()
85
+ if not k.startswith("_")
86
+ )
87
+ return f"{self.__class__.__name__}({txt})"
88
+
64
89
 
65
- @dataclass
66
90
  class NorgeIBilderWms(WmsLoader):
67
- """Loads Norge i bilder tiles as folium.WmsTiles."""
91
+ """Loads Norge i bilder tiles as folium.WmsTiles.
92
+
93
+ Args:
94
+ years: list of years to search for images.
95
+ contains: substrings to include in image search.
96
+ not_contains: substrings to exclude in image search.
97
+ show: Whether to show all layers upon initialisation of the map.
98
+ _use_json: Whether to use pre-made json file of image names and bounds/masks
99
+ if all years are within range. Defaults to True (much faster).
100
+ """
68
101
 
69
- years: Iterable[int | str] = DEFAULT_YEARS
70
- contains: str | Iterable[str] | None = None
71
- not_contains: str | Iterable[str] | None = None
72
- show: bool | Iterable[int] | int = False
73
- _use_json: bool = True
74
102
  url: str = "https://wms.geonorge.no/skwms1/wms.nib-prosjekter"
75
103
 
104
+ def __init__(
105
+ self,
106
+ years: Iterable[int | str] = DEFAULT_YEARS,
107
+ contains: str | Iterable[str] | None = None,
108
+ not_contains: str | Iterable[str] | None = None,
109
+ show: bool | Iterable[int] | int = False,
110
+ _use_json: bool = True,
111
+ ) -> None:
112
+ """Initialiser."""
113
+ self.years = [int(year) for year in years]
114
+ self.contains = contains
115
+ self.not_contains = not_contains
116
+ self.show = show
117
+ self._use_json = _use_json
118
+
119
+ if self._use_json and all(year in JSON_YEARS for year in self.years):
120
+ self._load_from_json()
121
+ else:
122
+ self._tiles = None
123
+
76
124
  def load_tiles(self, verbose: bool = False) -> None:
77
- """Load all Norge i bilder tiles into self.tiles."""
125
+ """Load all Norge i bilder tiles into self._tiles."""
78
126
  name_pattern = r"<Name>(.*?)</Name>"
79
127
  bbox_pattern = (
80
128
  r"<EX_GeographicBoundingBox>.*?"
@@ -105,14 +153,20 @@ class NorgeIBilderWms(WmsLoader):
105
153
 
106
154
  if (
107
155
  not name
108
- or not any(year in name for year in self.years)
156
+ or not any(str(year) in name for year in self.years)
109
157
  or (
110
158
  self.contains
111
- and not any(re.search(x, name.lower()) for x in self.contains)
159
+ and not any(
160
+ re.search(x, name.lower())
161
+ for x in _string_as_list(self.contains)
162
+ )
112
163
  )
113
164
  or (
114
165
  self.not_contains
115
- and any(re.search(x, name.lower()) for x in self.not_contains)
166
+ and any(
167
+ re.search(x, name.lower())
168
+ for x in _string_as_list(self.not_contains)
169
+ )
116
170
  )
117
171
  ):
118
172
  continue
@@ -131,10 +185,10 @@ class NorgeIBilderWms(WmsLoader):
131
185
 
132
186
  all_tiles.append(this_tile)
133
187
 
134
- self.tiles = sorted(all_tiles, key=lambda x: (x["year"]))
188
+ self._tiles = sorted(all_tiles, key=lambda x: (x["year"]))
135
189
 
136
190
  masks = self._get_norge_i_bilder_polygon_masks(verbose=verbose)
137
- for tile in self.tiles:
191
+ for tile in self._tiles:
138
192
  mask = masks.get(tile["name"], None)
139
193
  tile["geometry"] = mask
140
194
 
@@ -143,7 +197,7 @@ class NorgeIBilderWms(WmsLoader):
143
197
  from owslib.wms import WebMapService
144
198
  from PIL import Image
145
199
 
146
- relevant_names: dict[str, str] = {x["name"]: x["bbox"] for x in self.tiles}
200
+ relevant_names: dict[str, str] = {x["name"]: x["bbox"] for x in self._tiles}
147
201
  assert len(relevant_names), relevant_names
148
202
 
149
203
  url = "https://wms.geonorge.no/skwms1/wms.nib-mosaikk?SERVICE=WMS&REQUEST=GetCapabilities"
@@ -214,7 +268,7 @@ class NorgeIBilderWms(WmsLoader):
214
268
 
215
269
  def get_tiles(self, mask: Any, max_zoom: int = 40) -> list[folium.WmsTileLayer]:
216
270
  """Get all Norge i bilder tiles intersecting with a mask (bbox or polygon)."""
217
- if self.tiles is None:
271
+ if self._tiles is None:
218
272
  self.load_tiles()
219
273
 
220
274
  if not isinstance(mask, (GeoSeries | GeoDataFrame | Geometry)):
@@ -225,7 +279,7 @@ class NorgeIBilderWms(WmsLoader):
225
279
  else:
226
280
  show = False
227
281
 
228
- relevant_tiles = self._filter_tiles(mask)
282
+ relevant_tiles = self.filter_tiles(mask)
229
283
  tile_layers = {
230
284
  name: folium.WmsTileLayer(
231
285
  url="https://wms.geonorge.no/skwms1/wms.nib-prosjekter",
@@ -238,7 +292,7 @@ class NorgeIBilderWms(WmsLoader):
238
292
  show=show,
239
293
  max_zoom=max_zoom,
240
294
  )
241
- for name in relevant_tiles["name"]
295
+ for name in relevant_tiles
242
296
  }
243
297
 
244
298
  if not len(tile_layers):
@@ -254,59 +308,51 @@ class NorgeIBilderWms(WmsLoader):
254
308
 
255
309
  return tile_layers
256
310
 
257
- def _filter_tiles(self, mask):
311
+ def filter_tiles(
312
+ self, mask: GeoDataFrame | GeoSeries | Geometry | tuple[float]
313
+ ) -> list[str]:
258
314
  """Filter relevant dates with pandas and geopandas because fast."""
259
- if not self.tiles:
260
- return pd.DataFrame(columns=["name", "year", "geometry", "bbox"])
261
- df = pd.DataFrame(self.tiles)
262
- filt = (df["name"].notna()) & (df["year"].str.contains("|".join(self.years)))
315
+ if not self._tiles:
316
+ return []
317
+ df = pd.DataFrame(self._tiles)
318
+ filt = (df["name"].notna()) & (
319
+ df["year"].str.contains("|".join([str(year) for year in self.years]))
320
+ )
263
321
  if self.contains:
264
- for x in self.contains:
322
+ for x in _string_as_list(self.contains):
265
323
  filt &= df["name"].str.contains(x)
266
324
  if self.not_contains:
267
- for x in self.not_contains:
325
+ for x in _string_as_list(self.not_contains):
268
326
  filt &= ~df["name"].str.contains(x)
269
327
  df = df[filt]
270
328
  geoms = np.where(df["geometry"].notna(), df["geometry"], df["bbox"])
271
329
  geoms = GeoSeries(geoms)
272
330
  assert geoms.index.is_unique
273
- return df.iloc[sfilter(geoms, mask).index]
274
-
275
- def __post_init__(self) -> None:
276
- """Fix typings."""
277
- if self.contains and isinstance(self.contains, str):
278
- self.contains = [self.contains]
279
- elif self.contains:
280
- self.contains = [x for x in self.contains]
281
- if self.not_contains and isinstance(self.not_contains, str):
282
- self.not_contains = [self.not_contains]
283
- elif self.not_contains:
284
- self.not_contains = [x for x in self.not_contains]
331
+ return list(df.iloc[sfilter(geoms, mask).index]["name"])
332
+
333
+ def _load_from_json(self) -> None:
334
+ """Load tiles from json file."""
335
+ try:
336
+ with open(JSON_PATH, encoding="utf-8") as file:
337
+ self._tiles = json.load(file)
338
+ except FileNotFoundError:
339
+ self._tiles = None
340
+ return
341
+ self._tiles = [
342
+ {
343
+ key: (
344
+ value
345
+ if key not in ["bbox", "geometry"]
346
+ else shapely.wkt.loads(value)
347
+ )
348
+ for key, value in tile.items()
349
+ }
350
+ for tile in self._tiles
351
+ if any(str(year) in tile["name"] for year in self.years)
352
+ ]
285
353
 
286
- self.years = [str(int(year)) for year in self.years]
287
354
 
288
- if self._use_json and all(year in JSON_YEARS for year in self.years):
289
- try:
290
- with open(JSON_PATH, encoding="utf-8") as file:
291
- self.tiles = json.load(file)
292
- except FileNotFoundError:
293
- self.tiles = None
294
- return
295
- self.tiles = [
296
- {
297
- key: (
298
- value
299
- if key not in ["bbox", "geometry"]
300
- else shapely.wkt.loads(value)
301
- )
302
- for key, value in tile.items()
303
- }
304
- for tile in self.tiles
305
- if any(str(year) in tile["name"] for year in self.years)
306
- ]
307
- else:
308
- self.tiles = None
309
-
310
- def __repr__(self) -> str:
311
- """Print representation."""
312
- return f"{self.__class__.__name__}({len(self.tiles or [])})"
355
+ def _string_as_list(x: str | list[str]) -> list[str] | None:
356
+ if not x:
357
+ return None
358
+ return [x] if isinstance(x, str) else list(x)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes