ssb-sgis 1.1.12__tar.gz → 1.1.15__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 (65) hide show
  1. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/PKG-INFO +1 -1
  2. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/pyproject.toml +1 -1
  3. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/polygon_operations.py +8 -13
  4. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/io/dapla_functions.py +26 -15
  5. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/explore.py +1 -1
  6. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/map.py +2 -0
  7. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/raster/image_collection.py +82 -18
  8. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/LICENSE +0 -0
  9. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/README.md +0 -0
  10. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/__init__.py +0 -0
  11. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/conf.py +0 -0
  12. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/debug_config.py +0 -0
  13. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/exceptions.py +0 -0
  14. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/__init__.py +0 -0
  15. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/bounds.py +0 -0
  16. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/buffer_dissolve_explode.py +0 -0
  17. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/centerlines.py +0 -0
  18. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/cleaning.py +0 -0
  19. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/conversion.py +0 -0
  20. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/duplicates.py +0 -0
  21. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/general.py +0 -0
  22. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/geocoding.py +0 -0
  23. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/geometry_types.py +0 -0
  24. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/neighbors.py +0 -0
  25. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/overlay.py +0 -0
  26. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/point_operations.py +0 -0
  27. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/polygons_as_rings.py +0 -0
  28. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/geopandas_tools/sfilter.py +0 -0
  29. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/helpers.py +0 -0
  30. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/io/__init__.py +0 -0
  31. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/io/_is_dapla.py +0 -0
  32. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/io/opener.py +0 -0
  33. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/io/read_parquet.py +0 -0
  34. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/__init__.py +0 -0
  35. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/examine.py +0 -0
  36. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/httpserver.py +0 -0
  37. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/legend.py +0 -0
  38. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/maps.py +0 -0
  39. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/norge_i_bilder.json +0 -0
  40. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/thematicmap.py +0 -0
  41. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/tilesources.py +0 -0
  42. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/maps/wms.py +0 -0
  43. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/__init__.py +0 -0
  44. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/_get_route.py +0 -0
  45. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/_od_cost_matrix.py +0 -0
  46. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/_points.py +0 -0
  47. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/_service_area.py +0 -0
  48. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/closing_network_holes.py +0 -0
  49. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/cutting_lines.py +0 -0
  50. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/directednetwork.py +0 -0
  51. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/finding_isolated_networks.py +0 -0
  52. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/network.py +0 -0
  53. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/networkanalysis.py +0 -0
  54. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/networkanalysisrules.py +0 -0
  55. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/nodes.py +0 -0
  56. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/networkanalysis/traveling_salesman.py +0 -0
  57. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/parallel/__init__.py +0 -0
  58. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/parallel/parallel.py +0 -0
  59. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/py.typed +0 -0
  60. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/raster/__init__.py +0 -0
  61. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/raster/base.py +0 -0
  62. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/raster/indices.py +0 -0
  63. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/raster/regex.py +0 -0
  64. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/src/sgis/raster/sentinel_config.py +0 -0
  65. {ssb_sgis-1.1.12 → ssb_sgis-1.1.15}/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.1.12
3
+ Version: 1.1.15
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.1.12"
3
+ version = "1.1.15"
4
4
  description = "GIS functions used at Statistics Norway."
5
5
  authors = ["Morten Letnes <morten.letnes@ssb.no>"]
6
6
  license = "MIT"
@@ -205,9 +205,9 @@ def get_polygon_clusters(
205
205
 
206
206
  def get_cluster_mapper(
207
207
  gdf: GeoDataFrame | GeoSeries, predicate: str = "intersects"
208
- ) -> dict[int, int]:
209
- if not gdf.index.is_unique:
210
- raise ValueError("Index must be unique")
208
+ ) -> list[int]:
209
+ """Returns a list of cluster indices corresponding to the order of the input GeoDataFrame or GeoSeries."""
210
+ gdf = gdf.reset_index(drop=True)
211
211
  neighbors = get_neighbor_indices(gdf, gdf, predicate=predicate)
212
212
 
213
213
  edges = [(source, target) for source, target in neighbors.items()]
@@ -215,11 +215,12 @@ def get_cluster_mapper(
215
215
  graph = nx.Graph()
216
216
  graph.add_edges_from(edges)
217
217
 
218
- return {
219
- j: i
220
- for i, component in enumerate(nx.connected_components(graph))
221
- for j in component
218
+ mapper = {
219
+ i: cluster_index
220
+ for cluster_index, component in enumerate(nx.connected_components(graph))
221
+ for i in component
222
222
  }
223
+ return list(dict(sorted(mapper.items())).values())
223
224
 
224
225
 
225
226
  def eliminate_by_longest(
@@ -425,12 +426,6 @@ def eliminate_by_longest(
425
426
  explore_locals(center=_DEBUG_CONFIG["center"])
426
427
 
427
428
  if not _recurse and len(isolated):
428
- if 0:
429
- isolated.geometry = isolated.buffer(
430
- -PRECISION,
431
- resolution=1,
432
- join_style=2,
433
- )
434
429
  out, isolated = _recursively_eliminate_new_neighbors(
435
430
  out,
436
431
  isolated,
@@ -109,7 +109,7 @@ def read_geopandas(
109
109
  if single_eq_filter:
110
110
  try:
111
111
  expression = "".join(next(iter(filters))).replace("==", "=")
112
- glob_func = _get_glob(file_system)
112
+ glob_func = _get_glob_func(file_system)
113
113
  paths = glob_func(str(Path(gcs_path) / expression))
114
114
  if paths:
115
115
  return _read_geopandas_from_iterable(
@@ -543,7 +543,7 @@ def _write_partitioned_geoparquet(
543
543
  if df[col].isna().all() and not kwargs.get("schema"):
544
544
  raise ValueError("Must specify 'schema' when all rows are NA.")
545
545
 
546
- glob_func = _get_glob(file_system)
546
+ glob_func = _get_glob_func(file_system)
547
547
 
548
548
  if file_system.exists(path) and file_system.isfile(path):
549
549
  _remove_file(path, file_system)
@@ -596,7 +596,7 @@ def _write_partitioned_geoparquet(
596
596
  executor.map(threaded_write, dfs, paths)
597
597
 
598
598
 
599
- def _get_glob(file_system) -> functools.partial:
599
+ def _get_glob_func(file_system) -> functools.partial:
600
600
  try:
601
601
  return functools.partial(file_system.glob)
602
602
  except AttributeError:
@@ -724,9 +724,9 @@ def _read_partitioned_parquet(
724
724
  **kwargs,
725
725
  ):
726
726
  file_system = _get_file_system(file_system, kwargs)
727
+ glob_func = _get_glob_func(file_system)
727
728
 
728
729
  if child_paths is None:
729
- glob_func = _get_glob(file_system)
730
730
  child_paths = list(glob_func(str(Path(path) / "**/*.parquet")))
731
731
 
732
732
  filters = _filters_to_expression(filters)
@@ -735,18 +735,29 @@ def _read_partitioned_parquet(
735
735
  bbox, _ = _get_bounds_parquet_from_open_file(file, file_system)
736
736
  return shapely.box(*bbox).intersects(to_shapely(mask))
737
737
 
738
- def read(path: str) -> pyarrow.Table | None:
739
- with file_system.open(path, "rb") as file:
740
- if mask is not None and not intersects(file, mask):
741
- return
738
+ def read(child_path: str) -> pyarrow.Table | None:
739
+ try:
740
+ with file_system.open(child_path, "rb") as file:
741
+ if mask is not None and not intersects(file, mask):
742
+ return
742
743
 
743
- # 'get' instead of 'pop' because dict is mutable
744
- schema = kwargs.get("schema", pq.read_schema(file))
745
- new_kwargs = {
746
- key: value for key, value in kwargs.items() if key != "schema"
747
- }
744
+ # 'get' instead of 'pop' because dict is mutable
745
+ schema = kwargs.get("schema", pq.read_schema(file))
746
+ new_kwargs = {
747
+ key: value for key, value in kwargs.items() if key != "schema"
748
+ }
748
749
 
749
- return read_func(file, schema=schema, filters=filters, **new_kwargs)
750
+ return read_func(file, schema=schema, filters=filters, **new_kwargs)
751
+ except ArrowInvalid as e:
752
+ if not len(
753
+ {
754
+ x
755
+ for x in glob_func(str(Path(child_path) / "**"))
756
+ if not paths_are_equal(child_path, x)
757
+ }
758
+ ):
759
+ raise e
760
+ # allow not being able to read hard-to-delete empty directories
750
761
 
751
762
  with ThreadPoolExecutor() as executor:
752
763
  results = [
@@ -790,7 +801,7 @@ def paths_are_equal(path1: Path | str, path2: Path | str) -> bool:
790
801
 
791
802
 
792
803
  def get_child_paths(path, file_system) -> list[str]:
793
- glob_func = _get_glob(file_system)
804
+ glob_func = _get_glob_func(file_system)
794
805
  return [
795
806
  x
796
807
  for x in glob_func(str(Path(path) / "**/*.parquet"))
@@ -828,7 +828,7 @@ class Explore(Map):
828
828
  if not len(gdf):
829
829
  continue
830
830
 
831
- gdf = self._to_single_geom_type(make_all_singlepart(gdf))
831
+ gdf = self._to_single_geom_type(make_all_singlepart(gdf, ignore_index=True))
832
832
 
833
833
  if not len(gdf):
834
834
  continue
@@ -729,6 +729,8 @@ class Map:
729
729
  """Place the column values into groups."""
730
730
  bins = bins.copy()
731
731
 
732
+ assert gdf.index.is_unique
733
+
732
734
  # if equal lenght, convert to integer and check for equality
733
735
  if len(bins) == len(self._unique_values):
734
736
  if gdf[self._column].isna().all():
@@ -2,6 +2,7 @@ import datetime
2
2
  import functools
3
3
  import glob
4
4
  import itertools
5
+ import json
5
6
  import os
6
7
  import random
7
8
  import re
@@ -468,8 +469,8 @@ class _ImageBase:
468
469
  regexes = (regexes,)
469
470
  return tuple(re.compile(regexes, flags=re.VERBOSE) for regexes in regexes)
470
471
 
471
- @staticmethod
472
472
  def _metadata_to_nested_dict(
473
+ self,
473
474
  metadata: str | Path | os.PathLike | dict | pd.DataFrame | None,
474
475
  ) -> dict[str, dict[str, Any]]:
475
476
  """Construct metadata dict from dictlike, DataFrame or file path.
@@ -896,9 +897,6 @@ class Band(_ImageBandBase):
896
897
  if self._bounds is None:
897
898
  raise ValueError("Must specify bounds when data is an array.")
898
899
  if not (res is None or (callable(res) and res() is None)):
899
- # if not (res is None or (callable(res) and res() is None)) and _res_as_tuple(
900
- # res
901
- # ) != _get_res_from_bounds(self._bounds, data.shape):
902
900
  raise ValueError(
903
901
  f"Cannot specify 'res' when data is an array. {res} and {_get_res_from_bounds(self._bounds, data.shape)}"
904
902
  )
@@ -932,14 +930,16 @@ class Band(_ImageBandBase):
932
930
  }
933
931
 
934
932
  if self.metadata:
935
- if self.path is not None:
936
- self.metadata = {
937
- key: value
938
- for key, value in self.metadata.items()
939
- if key == self.path
940
- }
941
- this_metadata = self.metadata[self.path]
942
- for key, value in this_metadata.items():
933
+ parent = _fix_path(str(Path(self.path).parent))
934
+ for key, value in self.metadata.get(parent, {}).items():
935
+ if key == "bands" and self.band_id in value:
936
+ band_metadata = value[self.band_id]
937
+ for band_key, band_value in band_metadata.items():
938
+ if band_key in dir(self):
939
+ setattr(self, f"_{band_key}", band_value)
940
+ else:
941
+ setattr(self, band_key, band_value)
942
+ continue
943
943
  if key in dir(self):
944
944
  setattr(self, f"_{key}", value)
945
945
  else:
@@ -1592,6 +1592,22 @@ class Image(_ImageBandBase):
1592
1592
  else:
1593
1593
  self._all_file_paths = None
1594
1594
 
1595
+ if not self.metadata and "metadata.json" in {
1596
+ Path(x).name for x in self._all_file_paths
1597
+ }:
1598
+ with _open_func(
1599
+ next(
1600
+ iter(
1601
+ {
1602
+ x
1603
+ for x in self._all_file_paths
1604
+ if str(x).endswith("metadata.json")
1605
+ }
1606
+ )
1607
+ )
1608
+ ) as file:
1609
+ self.metadata = json.load(file)
1610
+
1595
1611
  if df is None:
1596
1612
  if not self._all_file_paths:
1597
1613
  self._all_file_paths = {self.path}
@@ -1616,12 +1632,10 @@ class Image(_ImageBandBase):
1616
1632
  key: value for key, value in self.metadata.items() if self.path in key
1617
1633
  }
1618
1634
 
1619
- if self.metadata:
1620
- try:
1621
- metadata = self.metadata[self.path]
1622
- except KeyError as e:
1623
- metadata = {}
1624
- for key, value in metadata.items():
1635
+ if self.metadata.get(self.path, {}):
1636
+ for key, value in self.metadata[self.path].items():
1637
+ if key in {"bands"}:
1638
+ continue
1625
1639
  if key in dir(self):
1626
1640
  setattr(self, f"_{key}", value)
1627
1641
  else:
@@ -1705,6 +1719,56 @@ class Image(_ImageBandBase):
1705
1719
 
1706
1720
  return self
1707
1721
 
1722
+ def get_image_metadata_dict(self) -> dict:
1723
+ """Creates a nested dict of metadata.
1724
+
1725
+ The dict structure will be:
1726
+
1727
+ {
1728
+ image_path: {
1729
+ image_attribute: value,
1730
+ ...,
1731
+ "bands": {
1732
+ band_id: {
1733
+ band_attribute: band_value,
1734
+ },
1735
+ ...,
1736
+ }
1737
+ }
1738
+ }
1739
+ """
1740
+ path = self.path
1741
+ metadata = {
1742
+ path: {
1743
+ "bounds": self.bounds,
1744
+ "crs": str(pyproj.CRS(self.crs).to_string()),
1745
+ }
1746
+ }
1747
+ for key in self.metadata_attributes:
1748
+ metadata[path][key] = getattr(self, key)
1749
+
1750
+ metadata[path]["bands"] = {}
1751
+ for band in self:
1752
+ metadata[path]["bands"][band.band_id] = {}
1753
+ for key in band.metadata_attributes:
1754
+ if key in self.metadata_attributes:
1755
+ continue
1756
+ metadata[path]["bands"][band.band_id][key] = getattr(band, key)
1757
+ return metadata
1758
+
1759
+ def write_image_metadata(self) -> None:
1760
+ """Write file 'metadata.json' under image path.
1761
+
1762
+ The file will be used to give the image attributes
1763
+ and avoid the much slower metadata fetching with rasterio.
1764
+
1765
+ See method 'get_image_metadata_dict' for info on the structure of
1766
+ the json file.
1767
+ """
1768
+ metadata = self.get_image_metadata_dict()
1769
+ with _open_func(str(Path(self.path) / "metadata.json"), "w") as file:
1770
+ json.dump(metadata, file)
1771
+
1708
1772
  def _construct_image_from_bands(
1709
1773
  self, data: Sequence[Band], res: int | None
1710
1774
  ) -> None:
File without changes
File without changes
File without changes
File without changes
File without changes