ssb-sgis 0.2.5__py3-none-any.whl → 0.2.7__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/__init__.py +1 -0
- sgis/dapla.py +18 -14
- sgis/geopandas_tools/polygon_operations.py +124 -55
- sgis/geopandas_tools/to_geodataframe.py +6 -2
- sgis/maps/examine.py +192 -0
- sgis/maps/explore.py +52 -2
- sgis/maps/map.py +2 -0
- {ssb_sgis-0.2.5.dist-info → ssb_sgis-0.2.7.dist-info}/METADATA +1 -1
- {ssb_sgis-0.2.5.dist-info → ssb_sgis-0.2.7.dist-info}/RECORD +11 -10
- {ssb_sgis-0.2.5.dist-info → ssb_sgis-0.2.7.dist-info}/LICENSE +0 -0
- {ssb_sgis-0.2.5.dist-info → ssb_sgis-0.2.7.dist-info}/WHEEL +0 -0
sgis/__init__.py
CHANGED
|
@@ -46,6 +46,7 @@ from .geopandas_tools.polygon_operations import (
|
|
|
46
46
|
)
|
|
47
47
|
from .geopandas_tools.to_geodataframe import to_gdf
|
|
48
48
|
from .helpers import get_name
|
|
49
|
+
from .maps.examine import Examine
|
|
49
50
|
from .maps.explore import Explore
|
|
50
51
|
from .maps.httpserver import run_html_server
|
|
51
52
|
from .maps.legend import Legend
|
sgis/dapla.py
CHANGED
|
@@ -47,22 +47,26 @@ def read_geopandas(path: str, **kwargs) -> GeoDataFrame:
|
|
|
47
47
|
"""
|
|
48
48
|
fs = dp.FileClient.get_gcs_file_system()
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
if "parquet" in path:
|
|
51
|
+
with fs.open(path, mode="rb") as file:
|
|
52
|
+
try:
|
|
53
53
|
return gpd.read_parquet(file, **kwargs)
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
except ValueError as e:
|
|
55
|
+
df = dp.read_pandas(path, **kwargs)
|
|
56
|
+
if not len(df):
|
|
57
|
+
return df
|
|
58
|
+
else:
|
|
59
|
+
raise e
|
|
60
|
+
else:
|
|
61
|
+
with fs.open(path, mode="rb") as file:
|
|
62
|
+
try:
|
|
56
63
|
return gpd.read_file(file, **kwargs)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
64
|
-
print(dp.FileClient().ls(parent))
|
|
65
|
-
raise e
|
|
64
|
+
except ValueError as e:
|
|
65
|
+
df = dp.read_pandas(path, **kwargs)
|
|
66
|
+
if not len(df):
|
|
67
|
+
return df
|
|
68
|
+
else:
|
|
69
|
+
raise e
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
def write_geopandas(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Functions for polygon geometries."""
|
|
2
|
+
import functools
|
|
2
3
|
import warnings
|
|
3
4
|
|
|
4
5
|
import geopandas as gpd
|
|
@@ -8,6 +9,7 @@ import pandas as pd
|
|
|
8
9
|
from geopandas import GeoDataFrame, GeoSeries
|
|
9
10
|
from shapely import (
|
|
10
11
|
area,
|
|
12
|
+
difference,
|
|
11
13
|
get_exterior_ring,
|
|
12
14
|
get_interior_ring,
|
|
13
15
|
get_num_interior_rings,
|
|
@@ -457,21 +459,19 @@ def get_overlapping_polygon_product(gdf: GeoDataFrame | GeoSeries) -> pd.Index:
|
|
|
457
459
|
return series
|
|
458
460
|
|
|
459
461
|
|
|
460
|
-
def
|
|
462
|
+
def close_all_holes(
|
|
461
463
|
gdf: GeoDataFrame | GeoSeries,
|
|
462
|
-
max_area: int | float,
|
|
463
464
|
*,
|
|
465
|
+
without_islands: bool = True,
|
|
464
466
|
copy: bool = True,
|
|
465
467
|
) -> GeoDataFrame | GeoSeries:
|
|
466
|
-
"""Closes holes in polygons
|
|
468
|
+
"""Closes all holes in polygons.
|
|
467
469
|
|
|
468
470
|
It takes a GeoDataFrame or GeoSeries of polygons and
|
|
469
|
-
|
|
470
|
-
either square meters ('max_m2') or square kilometers ('max_km2').
|
|
471
|
+
returns the outer circle.
|
|
471
472
|
|
|
472
473
|
Args:
|
|
473
474
|
gdf: GeoDataFrame or GeoSeries of polygons.
|
|
474
|
-
max_area: The maximum area in the unit of the GeoDataFrame's crs.
|
|
475
475
|
copy: if True (default), the input GeoDataFrame or GeoSeries is copied.
|
|
476
476
|
Defaults to True.
|
|
477
477
|
|
|
@@ -479,17 +479,11 @@ def close_small_holes(
|
|
|
479
479
|
A GeoDataFrame or GeoSeries of polygons with closed holes in the geometry
|
|
480
480
|
column.
|
|
481
481
|
|
|
482
|
-
Raises:
|
|
483
|
-
ValueError: If the coordinate reference system of the GeoDataFrame is not in
|
|
484
|
-
meter units.
|
|
485
|
-
ValueError: If both 'max_m2' and 'max_km2' is given.
|
|
486
|
-
|
|
487
482
|
Examples
|
|
488
483
|
--------
|
|
489
|
-
|
|
490
484
|
Let's create a circle with a hole in it.
|
|
491
485
|
|
|
492
|
-
>>> from sgis import
|
|
486
|
+
>>> from sgis import close_all_holes, buff, to_gdf
|
|
493
487
|
>>> point = to_gdf([260000, 6650000], crs=25833)
|
|
494
488
|
>>> point
|
|
495
489
|
geometry
|
|
@@ -501,50 +495,54 @@ def close_small_holes(
|
|
|
501
495
|
0 2.355807e+06
|
|
502
496
|
dtype: float64
|
|
503
497
|
|
|
504
|
-
Close
|
|
498
|
+
Close the hole.
|
|
505
499
|
|
|
506
|
-
>>> holes_closed =
|
|
500
|
+
>>> holes_closed = close_all_holes(circle_with_hole)
|
|
507
501
|
>>> holes_closed.area
|
|
508
502
|
0 3.141076e+06
|
|
509
503
|
dtype: float64
|
|
510
|
-
|
|
511
|
-
The hole will not be closed if it is larger.
|
|
512
|
-
|
|
513
|
-
>>> holes_closed = close_small_holes(circle_with_hole, max_area=1_000)
|
|
514
|
-
>>> holes_closed.area
|
|
515
|
-
0 2.355807e+06
|
|
516
|
-
dtype: float64
|
|
517
504
|
"""
|
|
518
|
-
if
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if isinstance(gdf, GeoDataFrame):
|
|
522
|
-
gdf["geometry"] = gdf.geometry.map(
|
|
523
|
-
lambda x: _close_small_holes_poly(x, max_area)
|
|
505
|
+
if not isinstance(gdf, (GeoDataFrame, GeoSeries)):
|
|
506
|
+
raise ValueError(
|
|
507
|
+
f"'gdf' should be of type GeoDataFrame or GeoSeries. Got {type(gdf)}"
|
|
524
508
|
)
|
|
525
|
-
return gdf
|
|
526
509
|
|
|
527
|
-
|
|
528
|
-
|
|
510
|
+
if copy:
|
|
511
|
+
gdf = gdf.copy()
|
|
529
512
|
|
|
513
|
+
if without_islands:
|
|
514
|
+
all_geoms = gdf.unary_union
|
|
515
|
+
if isinstance(gdf, GeoDataFrame):
|
|
516
|
+
gdf["geometry"] = gdf.geometry.map(
|
|
517
|
+
lambda x: _close_all_holes_no_islands(x, all_geoms)
|
|
518
|
+
)
|
|
519
|
+
return gdf
|
|
520
|
+
else:
|
|
521
|
+
return gdf.map(lambda x: _close_all_holes_no_islands(x, all_geoms))
|
|
530
522
|
else:
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
523
|
+
if isinstance(gdf, GeoDataFrame):
|
|
524
|
+
gdf["geometry"] = gdf.geometry.map(_close_all_holes)
|
|
525
|
+
return gdf
|
|
526
|
+
else:
|
|
527
|
+
return gdf.map(_close_all_holes)
|
|
534
528
|
|
|
535
529
|
|
|
536
|
-
def
|
|
530
|
+
def close_small_holes(
|
|
537
531
|
gdf: GeoDataFrame | GeoSeries,
|
|
532
|
+
max_area: int | float,
|
|
538
533
|
*,
|
|
534
|
+
without_islands: bool = True,
|
|
539
535
|
copy: bool = True,
|
|
540
536
|
) -> GeoDataFrame | GeoSeries:
|
|
541
|
-
"""Closes
|
|
537
|
+
"""Closes holes in polygons if the area is less than the given maximum.
|
|
542
538
|
|
|
543
539
|
It takes a GeoDataFrame or GeoSeries of polygons and
|
|
544
|
-
|
|
540
|
+
fills the holes that are smaller than the specified area given in units of
|
|
541
|
+
either square meters ('max_m2') or square kilometers ('max_km2').
|
|
545
542
|
|
|
546
543
|
Args:
|
|
547
544
|
gdf: GeoDataFrame or GeoSeries of polygons.
|
|
545
|
+
max_area: The maximum area in the unit of the GeoDataFrame's crs.
|
|
548
546
|
copy: if True (default), the input GeoDataFrame or GeoSeries is copied.
|
|
549
547
|
Defaults to True.
|
|
550
548
|
|
|
@@ -552,11 +550,17 @@ def close_all_holes(
|
|
|
552
550
|
A GeoDataFrame or GeoSeries of polygons with closed holes in the geometry
|
|
553
551
|
column.
|
|
554
552
|
|
|
553
|
+
Raises:
|
|
554
|
+
ValueError: If the coordinate reference system of the GeoDataFrame is not in
|
|
555
|
+
meter units.
|
|
556
|
+
ValueError: If both 'max_m2' and 'max_km2' is given.
|
|
557
|
+
|
|
555
558
|
Examples
|
|
556
559
|
--------
|
|
560
|
+
|
|
557
561
|
Let's create a circle with a hole in it.
|
|
558
562
|
|
|
559
|
-
>>> from sgis import
|
|
563
|
+
>>> from sgis import close_small_holes, buff, to_gdf
|
|
560
564
|
>>> point = to_gdf([260000, 6650000], crs=25833)
|
|
561
565
|
>>> point
|
|
562
566
|
geometry
|
|
@@ -568,35 +572,51 @@ def close_all_holes(
|
|
|
568
572
|
0 2.355807e+06
|
|
569
573
|
dtype: float64
|
|
570
574
|
|
|
571
|
-
Close
|
|
575
|
+
Close holes smaller than 1 square kilometer (1 million square meters).
|
|
572
576
|
|
|
573
|
-
>>> holes_closed =
|
|
577
|
+
>>> holes_closed = close_small_holes(circle_with_hole, max_area=1_000_000)
|
|
574
578
|
>>> holes_closed.area
|
|
575
579
|
0 3.141076e+06
|
|
576
580
|
dtype: float64
|
|
577
|
-
"""
|
|
578
|
-
if copy:
|
|
579
|
-
gdf = gdf.copy()
|
|
580
581
|
|
|
581
|
-
|
|
582
|
-
return unary_union(polygons(get_exterior_ring(get_parts(poly))))
|
|
582
|
+
The hole will not be closed if it is larger.
|
|
583
583
|
|
|
584
|
-
|
|
584
|
+
>>> holes_closed = close_small_holes(circle_with_hole, max_area=1_000)
|
|
585
|
+
>>> holes_closed.area
|
|
586
|
+
0 2.355807e+06
|
|
587
|
+
dtype: float64
|
|
588
|
+
"""
|
|
589
|
+
if not isinstance(gdf, (GeoSeries, GeoDataFrame)):
|
|
590
|
+
raise ValueError(
|
|
591
|
+
f"'gdf' should be of type GeoDataFrame or GeoSeries. Got {type(gdf)}"
|
|
592
|
+
)
|
|
585
593
|
|
|
586
|
-
if
|
|
587
|
-
gdf
|
|
588
|
-
return gdf
|
|
594
|
+
if copy:
|
|
595
|
+
gdf = gdf.copy()
|
|
589
596
|
|
|
590
|
-
|
|
591
|
-
|
|
597
|
+
if without_islands:
|
|
598
|
+
all_geoms = gdf.unary_union
|
|
592
599
|
|
|
600
|
+
if isinstance(gdf, GeoDataFrame):
|
|
601
|
+
gdf["geometry"] = gdf.geometry.map(
|
|
602
|
+
lambda x: _close_small_holes_no_islands(x, max_area, all_geoms)
|
|
603
|
+
)
|
|
604
|
+
return gdf
|
|
605
|
+
else:
|
|
606
|
+
return gdf.map(
|
|
607
|
+
lambda x: _close_small_holes_no_islands(x, max_area, all_geoms)
|
|
608
|
+
)
|
|
593
609
|
else:
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
610
|
+
if isinstance(gdf, GeoDataFrame):
|
|
611
|
+
gdf["geometry"] = gdf.geometry.map(
|
|
612
|
+
lambda x: _close_small_holes(x, max_area)
|
|
613
|
+
)
|
|
614
|
+
return gdf
|
|
615
|
+
else:
|
|
616
|
+
return gdf.map(lambda x: _close_small_holes(x, max_area))
|
|
597
617
|
|
|
598
618
|
|
|
599
|
-
def
|
|
619
|
+
def _close_small_holes(poly, max_area):
|
|
600
620
|
"""Closes cmall holes within one shapely geometry of polygons."""
|
|
601
621
|
|
|
602
622
|
# start with a list containing the polygon,
|
|
@@ -612,7 +632,56 @@ def _close_small_holes_poly(poly, max_area):
|
|
|
612
632
|
for n in range(n_interior_rings):
|
|
613
633
|
hole = polygons(get_interior_ring(part, n))
|
|
614
634
|
|
|
635
|
+
print(area(hole))
|
|
636
|
+
|
|
615
637
|
if area(hole) < max_area:
|
|
616
638
|
holes_closed.append(hole)
|
|
617
639
|
|
|
618
640
|
return unary_union(holes_closed)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _close_small_holes_no_islands(poly, max_area, all_geoms):
|
|
644
|
+
"""Closes small holes within one shapely geometry of polygons."""
|
|
645
|
+
|
|
646
|
+
# start with a list containing the polygon,
|
|
647
|
+
# then append all holes smaller than 'max_km2' to the list.
|
|
648
|
+
holes_closed = [poly]
|
|
649
|
+
singlepart = get_parts(poly)
|
|
650
|
+
for part in singlepart:
|
|
651
|
+
n_interior_rings = get_num_interior_rings(part)
|
|
652
|
+
|
|
653
|
+
if not (n_interior_rings):
|
|
654
|
+
continue
|
|
655
|
+
|
|
656
|
+
for n in range(n_interior_rings):
|
|
657
|
+
hole = polygons(get_interior_ring(part, n))
|
|
658
|
+
no_islands = unary_union(hole.difference(all_geoms))
|
|
659
|
+
if area(no_islands) < max_area:
|
|
660
|
+
holes_closed.append(no_islands)
|
|
661
|
+
|
|
662
|
+
return unary_union(holes_closed)
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _close_all_holes(poly):
|
|
666
|
+
return unary_union(polygons(get_exterior_ring(get_parts(poly))))
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _close_all_holes_no_islands(poly, all_geoms):
|
|
670
|
+
"""Closes all holes within one shapely geometry of polygons."""
|
|
671
|
+
|
|
672
|
+
# start with a list containing the polygon,
|
|
673
|
+
# then append all holes smaller than 'max_km2' to the list.
|
|
674
|
+
holes_closed = [poly]
|
|
675
|
+
singlepart = get_parts(poly)
|
|
676
|
+
for part in singlepart:
|
|
677
|
+
n_interior_rings = get_num_interior_rings(part)
|
|
678
|
+
|
|
679
|
+
if not (n_interior_rings):
|
|
680
|
+
continue
|
|
681
|
+
|
|
682
|
+
for n in range(n_interior_rings):
|
|
683
|
+
hole = polygons(get_interior_ring(part, n))
|
|
684
|
+
no_islands = unary_union(hole.difference(all_geoms))
|
|
685
|
+
holes_closed.append(no_islands)
|
|
686
|
+
|
|
687
|
+
return unary_union(holes_closed)
|
|
@@ -5,7 +5,7 @@ import geopandas as gpd
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
from geopandas import GeoDataFrame, GeoSeries
|
|
7
7
|
from pandas.api.types import is_array_like, is_dict_like, is_list_like
|
|
8
|
-
from shapely import Geometry, wkb, wkt
|
|
8
|
+
from shapely import Geometry, box, wkb, wkt
|
|
9
9
|
from shapely.geometry import Point
|
|
10
10
|
from shapely.ops import unary_union
|
|
11
11
|
|
|
@@ -168,6 +168,8 @@ def to_gdf(
|
|
|
168
168
|
geoseries = GeoSeries(
|
|
169
169
|
_make_shapely_geoms(list(geom.values())[0]), index=index
|
|
170
170
|
)
|
|
171
|
+
elif isinstance(geom, pd.Series):
|
|
172
|
+
geoseries = GeoSeries(_make_shapely_geoms(geom), index=index)
|
|
171
173
|
else:
|
|
172
174
|
geoseries = GeoSeries(_make_shapely_geoms(geom.iloc[:, 0]), index=index)
|
|
173
175
|
return GeoDataFrame({key: geoseries}, geometry=key, crs=crs, **kwargs)
|
|
@@ -292,9 +294,11 @@ def _make_one_shapely_geom(geom):
|
|
|
292
294
|
elif len(geom) == 2 or len(geom) == 3:
|
|
293
295
|
return Point(geom)
|
|
294
296
|
|
|
297
|
+
elif len(geom) == 4:
|
|
298
|
+
return box(*geom)
|
|
295
299
|
else:
|
|
296
300
|
raise ValueError(
|
|
297
301
|
"If 'geom' is an iterable, each item should consist of "
|
|
298
|
-
"wkt, wkb or
|
|
302
|
+
"wkt, wkb or (x, y (z) or bbox). Got ",
|
|
299
303
|
geom,
|
|
300
304
|
)
|
sgis/maps/examine.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import geopandas as gpd
|
|
2
|
+
|
|
3
|
+
from ..helpers import unit_is_degrees
|
|
4
|
+
from .maps import clipmap
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Examine:
|
|
8
|
+
"""Explore geometries one row at a time.
|
|
9
|
+
|
|
10
|
+
It takes one or more GeoDataFrames and shows an interactive map
|
|
11
|
+
of one area at the time with the 'next', 'prev' and 'current' methods.
|
|
12
|
+
|
|
13
|
+
After creating the examiner object, the 'next' method will create a map
|
|
14
|
+
showing all geometries within a given radius (the size parameter) of the
|
|
15
|
+
first geometry in 'mask_gdf' (or the first speficied gdf). The 'next' method
|
|
16
|
+
can then be repeated.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
*gdfs: One or more GeoDataFrames. The rows of the first GeoDataFrame
|
|
20
|
+
will be used as masks, unless 'mask_gdf' is specified.
|
|
21
|
+
column: Column to use as colors.
|
|
22
|
+
mask_gdf: Optional GeoDataFrame to use as mask iterator. The geometries
|
|
23
|
+
of mask_gdf will not be shown.
|
|
24
|
+
size: Number of meters (or other crs unit) to buffer the mask geometry
|
|
25
|
+
before clipping.
|
|
26
|
+
sort_values: Optional sorting column(s) of the mask GeoDataFrame. Rows
|
|
27
|
+
will be iterated through from the top.
|
|
28
|
+
**kwargs: Additional keyword arguments passed to sgis.clipmap.
|
|
29
|
+
|
|
30
|
+
Examples
|
|
31
|
+
--------
|
|
32
|
+
Create the examiner.
|
|
33
|
+
|
|
34
|
+
>>> import sgis as sg
|
|
35
|
+
>>> roads = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
|
|
36
|
+
>>> points = sg.read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet")
|
|
37
|
+
>>> e = sg.Examine(points, roads)
|
|
38
|
+
>>> e
|
|
39
|
+
|
|
40
|
+
Then the line below can be repeated for all rows if 'points'. This has to be
|
|
41
|
+
in a separate notebook cell to the previous.
|
|
42
|
+
|
|
43
|
+
>>> e.next()
|
|
44
|
+
|
|
45
|
+
Previous geometry:
|
|
46
|
+
|
|
47
|
+
>>> e.prev()
|
|
48
|
+
|
|
49
|
+
Repeating the current area with another layer and new column:
|
|
50
|
+
|
|
51
|
+
>>> some_points = points.sample(100)
|
|
52
|
+
>>> e.current(some_points, column="idx")
|
|
53
|
+
|
|
54
|
+
The row number can also be specified manually.
|
|
55
|
+
Can be done in 'next', 'prev' and 'current'.
|
|
56
|
+
|
|
57
|
+
>>> e.next(i=101)
|
|
58
|
+
|
|
59
|
+
This will create an examiner where 'points' is not shown, only used as mask.
|
|
60
|
+
|
|
61
|
+
>>> e = sg.Examine(roads, mask_gdf=points, column="oneway")
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
*gdfs: gpd.GeoDataFrame,
|
|
67
|
+
column: str | None = None,
|
|
68
|
+
mask_gdf: gpd.GeoDataFrame | None = None,
|
|
69
|
+
sort_values: str | None = None,
|
|
70
|
+
size: int | float = 1000,
|
|
71
|
+
**kwargs,
|
|
72
|
+
):
|
|
73
|
+
if not all(isinstance(gdf, gpd.GeoDataFrame) for gdf in gdfs):
|
|
74
|
+
raise ValueError("gdfs must be of type GeoDataFrame.")
|
|
75
|
+
|
|
76
|
+
self.gdfs = gdfs
|
|
77
|
+
if mask_gdf is None:
|
|
78
|
+
self.mask_gdf = gdfs[0]
|
|
79
|
+
else:
|
|
80
|
+
self.mask_gdf = mask_gdf
|
|
81
|
+
|
|
82
|
+
if unit_is_degrees(self.mask_gdf) and size > 360:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"CRS unit is degrees. Use geopandas' "
|
|
85
|
+
"to_crs method to change crs to e.g. UTM. "
|
|
86
|
+
"Or set 'size' to a smaller number."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if sort_values is not None:
|
|
90
|
+
self.mask_gdf = self.mask_gdf.sort_values(sort_values)
|
|
91
|
+
|
|
92
|
+
self.indices = list(range(len(gdfs[0])))
|
|
93
|
+
self.i = 0
|
|
94
|
+
self.column = column
|
|
95
|
+
self.size = size
|
|
96
|
+
self.kwargs = kwargs
|
|
97
|
+
|
|
98
|
+
def next(self, *gdfs, i: int | None = None, **kwargs):
|
|
99
|
+
"""Displays a map of geometries within the next row of the mask gdf.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
*gdfs: Optional GeoDataFrames to be added on top of the current.
|
|
103
|
+
i: Optionally set the integer index of which row to use as mask.
|
|
104
|
+
**kwargs: Additional keyword arguments passed to sgis.clipmap.
|
|
105
|
+
"""
|
|
106
|
+
gdfs = () if not gdfs else gdfs
|
|
107
|
+
self.gdfs = self.gdfs + gdfs
|
|
108
|
+
if kwargs:
|
|
109
|
+
kwargs = self._fix_kwargs(kwargs)
|
|
110
|
+
self.kwargs = self.kwargs | kwargs
|
|
111
|
+
|
|
112
|
+
if i:
|
|
113
|
+
self.i = i
|
|
114
|
+
|
|
115
|
+
if self.i >= len(self.mask_gdf):
|
|
116
|
+
print("All rows are shown.")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
print(f"{self.i + 1} of {len(self.mask_gdf)}")
|
|
120
|
+
clipmap(
|
|
121
|
+
*self.gdfs,
|
|
122
|
+
self.column,
|
|
123
|
+
mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
|
|
124
|
+
**self.kwargs,
|
|
125
|
+
)
|
|
126
|
+
self.i += 1
|
|
127
|
+
|
|
128
|
+
def prev(self, *gdfs, i: int | None = None, **kwargs):
|
|
129
|
+
"""Displays a map of geometries within the previus row of the mask gdf.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
*gdfs: Optional GeoDataFrames to be added on top of the current.
|
|
133
|
+
i: Optionally set the integer index of which row to use as mask.
|
|
134
|
+
**kwargs: Additional keyword arguments passed to sgis.clipmap.
|
|
135
|
+
"""
|
|
136
|
+
gdfs = () if not gdfs else gdfs
|
|
137
|
+
self.gdfs = self.gdfs + gdfs
|
|
138
|
+
if kwargs:
|
|
139
|
+
kwargs = self._fix_kwargs(kwargs)
|
|
140
|
+
self.kwargs = self.kwargs | kwargs
|
|
141
|
+
|
|
142
|
+
self.i -= 2
|
|
143
|
+
|
|
144
|
+
if i:
|
|
145
|
+
self.i = i
|
|
146
|
+
|
|
147
|
+
print(f"{self.i + 1} of {len(self.mask_gdf)}")
|
|
148
|
+
clipmap(
|
|
149
|
+
*self.gdfs,
|
|
150
|
+
self.column,
|
|
151
|
+
mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
|
|
152
|
+
**self.kwargs,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def current(self, *gdfs, i: int | None = None, **kwargs):
|
|
156
|
+
"""Repeat the last shown map."""
|
|
157
|
+
gdfs = () if not gdfs else gdfs
|
|
158
|
+
self.gdfs = self.gdfs + gdfs
|
|
159
|
+
if kwargs:
|
|
160
|
+
kwargs = self._fix_kwargs(kwargs)
|
|
161
|
+
self.kwargs = self.kwargs | kwargs
|
|
162
|
+
|
|
163
|
+
if i:
|
|
164
|
+
self.i = i
|
|
165
|
+
|
|
166
|
+
print(f"{self.i + 1} of {len(self.mask_gdf)}")
|
|
167
|
+
clipmap(
|
|
168
|
+
*self.gdfs,
|
|
169
|
+
self.column,
|
|
170
|
+
mask=self.mask_gdf.iloc[[self.i]].buffer(self.size),
|
|
171
|
+
**self.kwargs,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
def get_current_mask(self) -> gpd.GeoDataFrame:
|
|
175
|
+
"""Returns a GeoDataFrame of the last shown mask geometry."""
|
|
176
|
+
return self.mask_gdf.iloc[[self.i]]
|
|
177
|
+
|
|
178
|
+
def get_current_geoms(self) -> tuple[gpd.GeoDataFrame]:
|
|
179
|
+
"""Returns all GeoDataFrames in the area of the last shown mask geometry."""
|
|
180
|
+
mask = self.mask_gdf.iloc[[self.i]]
|
|
181
|
+
gdfs = ()
|
|
182
|
+
for gdf in self.gdfs:
|
|
183
|
+
gdfs = gdfs + (gdf.clip(mask.buffer(self.size)),)
|
|
184
|
+
return gdfs
|
|
185
|
+
|
|
186
|
+
def _fix_kwargs(self, kwargs) -> dict:
|
|
187
|
+
self.size = kwargs.pop("size", self.size)
|
|
188
|
+
self.column = kwargs.pop("column", self.column)
|
|
189
|
+
return kwargs
|
|
190
|
+
|
|
191
|
+
def __repr__(self) -> str:
|
|
192
|
+
return f"{self.__class__}(indices={len(self.indices)}, current={self.i}, n_gdfs={len(self.gdfs)})"
|
sgis/maps/explore.py
CHANGED
|
@@ -11,8 +11,10 @@ import folium
|
|
|
11
11
|
import matplotlib
|
|
12
12
|
import numpy as np
|
|
13
13
|
import pandas as pd
|
|
14
|
+
from folium import plugins
|
|
14
15
|
from geopandas import GeoDataFrame
|
|
15
16
|
from IPython.display import display
|
|
17
|
+
from jinja2 import Template
|
|
16
18
|
from shapely.geometry import LineString
|
|
17
19
|
|
|
18
20
|
from ..geopandas_tools.general import clean_geoms, make_all_singlepart
|
|
@@ -61,6 +63,30 @@ _MAP_KWARGS = [
|
|
|
61
63
|
]
|
|
62
64
|
|
|
63
65
|
|
|
66
|
+
class MeasureControlFix(plugins.MeasureControl):
|
|
67
|
+
"""To fix a bug in the lenght measurement control.
|
|
68
|
+
|
|
69
|
+
Kudos to abewartech (https://github.com/ljagis/leaflet-measure/issues/171).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
_template = Template(
|
|
73
|
+
"""
|
|
74
|
+
{% macro script(this, kwargs) %}
|
|
75
|
+
L.Control.Measure.include({ _setCaptureMarkerIcon: function () { this._captureMarker.options.autoPanOnFocus = false; this._captureMarker.setIcon( L.divIcon({ iconSize: this._map.getSize().multiplyBy(2), }), ); }, });
|
|
76
|
+
var {{ this.get_name() }} = new L.Control.Measure(
|
|
77
|
+
{{ this.options|tojson }});
|
|
78
|
+
{{this._parent.get_name()}}.addControl({{this.get_name()}});
|
|
79
|
+
|
|
80
|
+
{% endmacro %}
|
|
81
|
+
"""
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def __init__(self, active_color="red", completed_color="red", **kwargs):
|
|
85
|
+
super().__init__(
|
|
86
|
+
active_color=active_color, completed_color=completed_color, **kwargs
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
64
90
|
class Explore(Map):
|
|
65
91
|
def __init__(
|
|
66
92
|
self,
|
|
@@ -71,6 +97,8 @@ class Explore(Map):
|
|
|
71
97
|
smooth_factor: float = 1.5,
|
|
72
98
|
browser: bool = False,
|
|
73
99
|
prefer_canvas: bool = True,
|
|
100
|
+
measure_control: bool = True,
|
|
101
|
+
geocoder: bool = True,
|
|
74
102
|
save=None,
|
|
75
103
|
**kwargs,
|
|
76
104
|
):
|
|
@@ -86,6 +114,8 @@ class Explore(Map):
|
|
|
86
114
|
self.max_zoom = max_zoom
|
|
87
115
|
self.smooth_factor = smooth_factor
|
|
88
116
|
self.prefer_canvas = prefer_canvas
|
|
117
|
+
self.measure_control = measure_control
|
|
118
|
+
self.geocoder = geocoder
|
|
89
119
|
self.save = save
|
|
90
120
|
|
|
91
121
|
self._to_single_geom_type()
|
|
@@ -291,8 +321,8 @@ class Explore(Map):
|
|
|
291
321
|
self._categories_colors_dict.keys(),
|
|
292
322
|
self._categories_colors_dict.values(),
|
|
293
323
|
)
|
|
294
|
-
folium.TileLayer("stamentoner").add_to(self.map)
|
|
295
|
-
folium.TileLayer("cartodbdark_matter").add_to(self.map)
|
|
324
|
+
folium.TileLayer("stamentoner", max_zoom=self.max_zoom).add_to(self.map)
|
|
325
|
+
folium.TileLayer("cartodbdark_matter", max_zoom=self.max_zoom).add_to(self.map)
|
|
296
326
|
self.map.add_child(folium.LayerControl())
|
|
297
327
|
|
|
298
328
|
def _create_continous_map(self):
|
|
@@ -448,6 +478,26 @@ class Explore(Map):
|
|
|
448
478
|
**map_kwds,
|
|
449
479
|
)
|
|
450
480
|
|
|
481
|
+
if self.measure_control:
|
|
482
|
+
MeasureControlFix(
|
|
483
|
+
primary_length_unit="meters",
|
|
484
|
+
secondary_length_unit="kilometers",
|
|
485
|
+
primary_area_unit="sqmeters",
|
|
486
|
+
secondary_area_unit="sqkilometers",
|
|
487
|
+
position="bottomleft",
|
|
488
|
+
capture_z_index=False,
|
|
489
|
+
).add_to(m)
|
|
490
|
+
|
|
491
|
+
plugins.Fullscreen(
|
|
492
|
+
position="topleft",
|
|
493
|
+
title="Expand me",
|
|
494
|
+
title_cancel="Exit me",
|
|
495
|
+
force_separate_button=True,
|
|
496
|
+
).add_to(m)
|
|
497
|
+
|
|
498
|
+
if self.geocoder:
|
|
499
|
+
plugins.Geocoder(position="topright").add_to(m)
|
|
500
|
+
|
|
451
501
|
# fit bounds to get a proper zoom level
|
|
452
502
|
if fit and "zoom_start" not in kwargs:
|
|
453
503
|
m.fit_bounds([[bounds[1], bounds[0]], [bounds[3], bounds[2]]])
|
sgis/maps/map.py
CHANGED
|
@@ -467,6 +467,8 @@ class Map:
|
|
|
467
467
|
if gdf[self._column].isna().all():
|
|
468
468
|
return np.repeat(len(bins), len(gdf))
|
|
469
469
|
|
|
470
|
+
# need numpy.nan instead of pd.NA as of now
|
|
471
|
+
gdf[self._column] = gdf[self._column].fillna(np.nan)
|
|
470
472
|
classified = np.searchsorted(bins, gdf[self._column])
|
|
471
473
|
|
|
472
474
|
return classified
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
sgis/__init__.py,sha256=
|
|
2
|
-
sgis/dapla.py,sha256=
|
|
1
|
+
sgis/__init__.py,sha256=BWIQpUVt_X9jFJ6VkJO1XVcGQiOKv60PskNG5XO26G0,2368
|
|
2
|
+
sgis/dapla.py,sha256=BlJ62kLwpTTQtmbj0Yutbh-bwokVPXHVb3QsRlMugF8,3542
|
|
3
3
|
sgis/exceptions.py,sha256=ztMp4sB9xxPvwj2IEsO5kOaB4FmHuU_7-M2pZ7qaxTs,576
|
|
4
4
|
sgis/geopandas_tools/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
sgis/geopandas_tools/buffer_dissolve_explode.py,sha256=46uBomQJMDFtGWVNZWyZLl-NLMwYyhr4wWkwj6BTxoo,8080
|
|
@@ -8,14 +8,15 @@ sgis/geopandas_tools/geometry_types.py,sha256=o3MbBP-aI7hVWWKVr_5p91TDhjiqZ_2IGx
|
|
|
8
8
|
sgis/geopandas_tools/neighbors.py,sha256=tv8bmYgq4VNFbXmT2wcmJsFH8946NwbIBMQXAi3n8L4,14520
|
|
9
9
|
sgis/geopandas_tools/overlay.py,sha256=RSxrDF0sXs6ZMxbeBJC9HFBVM4yaz10-cdbq3SCosFQ,11862
|
|
10
10
|
sgis/geopandas_tools/point_operations.py,sha256=3JynroucouAbpON4DWG32S3MQQGmfIJuY7D6gkqtk70,6888
|
|
11
|
-
sgis/geopandas_tools/polygon_operations.py,sha256=
|
|
12
|
-
sgis/geopandas_tools/to_geodataframe.py,sha256=
|
|
11
|
+
sgis/geopandas_tools/polygon_operations.py,sha256=ZzRx57w4LodN8_P_Tqc-zBAxGCvgvObIi9kJQKa-LLc,22646
|
|
12
|
+
sgis/geopandas_tools/to_geodataframe.py,sha256=g1NqPVVH6hAdMupWxHVd10EvipLY9wPiahh2u_vLOnk,9887
|
|
13
13
|
sgis/helpers.py,sha256=OqTojkSl-JVKlJzqqB-d_0CH6mk7_LS1DkiIjp1gD8E,2674
|
|
14
14
|
sgis/maps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
sgis/maps/
|
|
15
|
+
sgis/maps/examine.py,sha256=Wf48WwyNyPC1vYdw-n7GpvJYhwd-YMd_VZLOUI5vWZ4,6479
|
|
16
|
+
sgis/maps/explore.py,sha256=mab8LE8c_dsGwS1gVCCA2A3r5_Vh7hnzEfEs5MBadvE,23631
|
|
16
17
|
sgis/maps/httpserver.py,sha256=7ksCSs-WlchcREgjdCZd6II-riJpox34HpVXsCzN_AU,1923
|
|
17
18
|
sgis/maps/legend.py,sha256=GXAqGOb_zAWcDavd5aHzRyRB7nTRhPCQfSupYA693lk,20499
|
|
18
|
-
sgis/maps/map.py,sha256=
|
|
19
|
+
sgis/maps/map.py,sha256=niK6N0eFJjAalxjHTNA7kh-2KuazLVsnWlu8i9Ava7o,18611
|
|
19
20
|
sgis/maps/maps.py,sha256=NaK_wu4RGf6kKRUnnY7gLtxAY9x0d6gKxgQLubDbgHY,15961
|
|
20
21
|
sgis/maps/thematicmap.py,sha256=6aVPciftW1YjxjRVQDipxayl3aI3tHpYiZ3HfpnSavc,14132
|
|
21
22
|
sgis/networkanalysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -33,7 +34,7 @@ sgis/networkanalysis/networkanalysisrules.py,sha256=BhhaSXIyBRNzxSOUP2kVBIR--TRq
|
|
|
33
34
|
sgis/networkanalysis/nodes.py,sha256=Ys3FjB39Pir3U0jOoLKIPxCC4psC9mdlqdC7G6dSJg0,6767
|
|
34
35
|
sgis/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
36
|
sgis/read_parquet.py,sha256=GSW2NDy4-XosbamPEzB1xhWxFAPHuGEJZglfQ-V6DzY,3774
|
|
36
|
-
ssb_sgis-0.2.
|
|
37
|
-
ssb_sgis-0.2.
|
|
38
|
-
ssb_sgis-0.2.
|
|
39
|
-
ssb_sgis-0.2.
|
|
37
|
+
ssb_sgis-0.2.7.dist-info/LICENSE,sha256=lL2h0dNKGTKAE0CjTy62SDbRennVD1xPgM5LzGqhKeo,1074
|
|
38
|
+
ssb_sgis-0.2.7.dist-info/METADATA,sha256=hpcq2ydyXosFFnw-35qJtrdL3a7bhdblNERuDUiQea8,8831
|
|
39
|
+
ssb_sgis-0.2.7.dist-info/WHEEL,sha256=7Z8_27uaHI_UZAc4Uox4PpBhQ9Y5_modZXWMxtUi4NU,88
|
|
40
|
+
ssb_sgis-0.2.7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|