ssb-sgis 0.1.4__py3-none-any.whl → 0.1.6__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.
@@ -234,7 +234,7 @@ def random_points_in_polygons(
234
234
 
235
235
  Examples
236
236
  --------
237
- Buffer 100 random points.
237
+ First create and buffer 100 random points.
238
238
 
239
239
  >>> import sgis as sg
240
240
  >>> gdf = sg.random_points(100)
@@ -311,9 +311,22 @@ def random_points_in_polygons(
311
311
  bounds = gdf__.bounds
312
312
  temp_idx____ = gdf__["temp_idx____"].values
313
313
 
314
+ all_points = all_points.sort_index().drop(["temp_idx____", "index_right"], axis=1)
315
+
316
+ if gdf.index.is_unique:
317
+ gdf = gdf.drop("temp_idx____", axis=1)
318
+ return all_points
319
+
320
+ original_index = {
321
+ temp_idx: idx for temp_idx, idx in zip(gdf.temp_idx____, gdf.index)
322
+ }
323
+
324
+ all_points.index = all_points.index.map(original_index)
325
+ all_points.index.name = None
326
+
314
327
  gdf = gdf.drop("temp_idx____", axis=1)
315
328
 
316
- return all_points.sort_index().drop(["temp_idx____", "index_right"], axis=1)
329
+ return all_points
317
330
 
318
331
 
319
332
  def points_in_bounds(gdf: GeoDataFrame, n2: int):
@@ -355,8 +368,8 @@ def to_lines(*gdfs: GeoDataFrame, copy: bool = True) -> GeoDataFrame:
355
368
  >>> poly1["poly1"] = 1
356
369
  >>> line = sg.to_lines(poly1)
357
370
  >>> line
358
- poly1 geometry
359
- 0 1 LINESTRING (0.00000 0.00000, 0.00000 1.00000, ...
371
+ geometry poly1
372
+ 0 LINESTRING (0.00000 0.00000, 0.00000 1.00000, ... 1
360
373
 
361
374
  Convert two overlapping polygons to linestrings.
362
375
 
@@ -372,13 +385,11 @@ def to_lines(*gdfs: GeoDataFrame, copy: bool = True) -> GeoDataFrame:
372
385
  4 NaN 1.0 LINESTRING (0.50000 1.00000, 0.50000 1.50000, ...
373
386
  5 NaN 1.0 LINESTRING (1.00000 0.50000, 0.50000 0.50000)
374
387
 
375
- Plot before and after (plots not showing in terminal).
388
+ Plot before and after.
376
389
 
377
390
  >>> sg.qtm(poly1, poly2)
378
- <Axes: >
379
391
  >>> lines["l"] = lines.length
380
392
  >>> sg.qtm(lines, "l")
381
- <Axes: >
382
393
  """
383
394
 
384
395
  if any(any(gdf.geom_type.isin(["Point", "MultiPoint"])) for gdf in gdfs):
@@ -60,7 +60,7 @@ def to_single_geom_type(
60
60
  0 GEOMETRYCOLLECTION (POINT (0.00000 0.00000), L...
61
61
 
62
62
  >>> to_single_geom_type(gdf, "line")
63
- geometry
63
+ geometry
64
64
  2 LINESTRING (1.00000 1.00000, 2.00000 2.00000)
65
65
  """
66
66
  if not isinstance(gdf, (GeoDataFrame, GeoSeries)):
@@ -698,7 +698,7 @@ def close_network_holes_to_deadends(
698
698
 
699
699
  Fill gaps shorter than 1.1 meters.
700
700
 
701
- >>> filled = sg.close_network_holes_to_deadends(roads, max_distance=1.1, max_angle=30)
701
+ >>> filled = sg.close_network_holes_to_deadends(roads, max_distance=1.1)
702
702
  >>> roads = sg.get_largest_component(roads)
703
703
  >>> roads.connected.value_counts()
704
704
  1.0 100315
@@ -77,6 +77,9 @@ def overlay(
77
77
  f"'how' was {how!r} but is expected to be in {', '.join(allowed_hows)}"
78
78
  )
79
79
 
80
+ if df1.crs != df2.crs:
81
+ raise ValueError(f"crs mismatch. Got {df1.crs} and {df2.crs}")
82
+
80
83
  df1 = clean_geoms(df1)
81
84
  df2 = clean_geoms(df2)
82
85
 
@@ -295,6 +298,8 @@ def _union(pairs, df1, df2, left, right):
295
298
  merged.append(intersections)
296
299
  symmdiff = _symmetric_difference(pairs, df1, df2, left, right)
297
300
  merged.append(symmdiff)
301
+ crs = merged[0].crs
302
+ merged = [gdf.to_crs(crs) for gdf in merged]
298
303
  return pd.concat(merged, ignore_index=True).pipe(_push_geom_col)
299
304
 
300
305
 
@@ -305,6 +310,8 @@ def _identity(pairs, df1, left):
305
310
  merged.append(intersections)
306
311
  diff = _difference(pairs, df1, left)
307
312
  merged.append(diff)
313
+ crs = merged[0].crs
314
+ merged = [gdf.to_crs(crs) for gdf in merged]
308
315
  return pd.concat(merged, ignore_index=True).pipe(_push_geom_col)
309
316
 
310
317
 
@@ -317,6 +324,8 @@ def _symmetric_difference(pairs, df1, df2, left, right):
317
324
  merged.append(clip_right)
318
325
  diff_right = _add_from_right(df1, df2, right)
319
326
  merged.append(diff_right)
327
+ crs = merged[0].crs
328
+ merged = [gdf.to_crs(crs) for gdf in merged]
320
329
  return pd.concat(merged, ignore_index=True).pipe(_push_geom_col)
321
330
 
322
331
 
@@ -327,6 +336,8 @@ def _difference(pairs, df1, left):
327
336
  merged.append(clip_left)
328
337
  diff_left = _add_from_left(df1, left)
329
338
  merged.append(diff_left)
339
+ crs = merged[0].crs
340
+ merged = [gdf.to_crs(crs) for gdf in merged]
330
341
  return pd.concat(merged, ignore_index=True).pipe(_push_geom_col)
331
342
 
332
343
 
sgis/maps/explore.py CHANGED
@@ -9,6 +9,7 @@ from statistics import mean
9
9
  import branca as bc
10
10
  import folium
11
11
  import matplotlib
12
+ import numpy as np
12
13
  import pandas as pd
13
14
  from geopandas import GeoDataFrame
14
15
  from shapely.geometry import LineString
@@ -30,7 +31,7 @@ NAN_COLOR = "#969696"
30
31
 
31
32
 
32
33
  # cols to not show when hovering over geometries (tooltip)
33
- COLS_TO_DROP = ["color", "geometry"]
34
+ COLS_TO_DROP = ["color", "col_as_int", "geometry"]
34
35
 
35
36
 
36
37
  # from geopandas
@@ -71,49 +72,19 @@ class Explore(Map):
71
72
  self.max_zoom = max_zoom
72
73
  self.show_in_browser = show_in_browser
73
74
 
74
- if not self._is_categorical:
75
+ if any(get_geom_type(gdf) == "mixed" for gdf in self.gdfs):
76
+ self._make_all_polygon()
77
+
78
+ if self._is_categorical:
79
+ if len(self.gdfs) == 1:
80
+ self._split_categories()
81
+ else:
75
82
  if not self._cmap:
76
83
  self._cmap = "viridis"
77
84
  self.cmap_start = kwargs.pop("cmap_start", 0)
78
85
  self.cmap_stop = kwargs.pop("cmap_stop", 256)
79
86
 
80
87
  def explore(self, column: str | None = None, **kwargs) -> None:
81
- """Interactive map of the GeoDataFrames with layers that can be toggles on/off.
82
-
83
- It displays all the GeoDataFrames and displays them together in an interactive
84
- map with a common legend. The layers can be toggled on and off.
85
-
86
- Args:
87
- column: The column to color the geometries by. Defaults to the column
88
- that was specified last.
89
- **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
90
- instance 'cmap' to change the colors, 'scheme' to change how the data
91
- is grouped. This defaults to 'fisherjenks' for numeric data.
92
-
93
- See also:
94
- samplemap: same functionality, but shows only a random area of a given size.
95
- clipmap: same functionality, but shows only the areas clipped by a given
96
- mask.
97
-
98
- Examples
99
- --------
100
- >>> from sgis import read_parquet_url
101
- >>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
102
- >>> points = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet")
103
-
104
- Simple explore of two GeoDataFrames.
105
-
106
- >>> from sgis import Explore
107
- >>> ex = Explore(roads, points)
108
- >>> ex.explore()
109
-
110
- With column.
111
-
112
- >>> roads["meters"] = roads.length
113
- >>> points["meters"] = points.length
114
- >>> ex = Explore(roads, points, column="meters")
115
- >>> ex.samplemap()
116
- """
117
88
  if column:
118
89
  self._column = column
119
90
  self._update_column()
@@ -128,34 +99,6 @@ class Explore(Map):
128
99
  sample_from_first: bool = True,
129
100
  **kwargs,
130
101
  ) -> None:
131
- """Shows an interactive map of a random area of the GeoDataFrames.
132
-
133
- It takes a random sample point of the GeoDataFrames, and shows all geometries
134
- within a given radius of this point. Displays an interactive map with a common
135
- legend. The layers can be toggled on and off.
136
-
137
- The radius to plot can be changed with the 'size' parameter.
138
-
139
- For more info about the labeling and coloring of the map, see the explore
140
- method.
141
-
142
- Args:
143
- size: the radius to buffer the sample point by before clipping with the
144
- data.
145
- column: The column to color the geometries by. Defaults to the column
146
- that was specified last.
147
- sample_from_first: If True (default), the sample point is taken from
148
- the first specified GeoDataFrame. If False, all GeoDataFrames are
149
- considered.
150
- **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
151
- instance 'cmap' to change the colors, 'scheme' to change how the data
152
- is grouped. This defaults to 'fisherjenks' for numeric data.
153
-
154
- See also:
155
- explore: same functionality, but shows the entire area of the geometries.
156
- clipmap: same functionality, but shows only the areas clipped by a given
157
- mask.
158
- """
159
102
  if column:
160
103
  self._column = column
161
104
  self._update_column()
@@ -193,27 +136,6 @@ class Explore(Map):
193
136
  column: str | None = None,
194
137
  **kwargs,
195
138
  ) -> None:
196
- """Shows an interactive map of a of the GeoDataFrames clipped by the mask.
197
-
198
- It clips all the GeoDataFrames in the Explore instance to the mask extent,
199
- and displays the resulting geometries in an interactive map with a common
200
- legends. The layers can be toggled on and off.
201
-
202
- For more info about the labeling and coloring of the map, see the explore
203
- method.
204
-
205
- Args:
206
- mask: the geometry to clip the data by.
207
- column: The column to color the geometries by. Defaults to the column
208
- that was specified last.
209
- **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
210
- instance 'cmap' to change the colors, 'scheme' to change how the data
211
- is grouped. This defaults to 'fisherjenks' for numeric data.
212
-
213
- See also:
214
- explore: same functionality, but shows the entire area of the geometries.
215
- samplemap: same functionality, but shows only a random area of a given size.
216
- """
217
139
  if column:
218
140
  self._column = column
219
141
  self._update_column()
@@ -222,16 +144,15 @@ class Explore(Map):
222
144
  gdfs: tuple[GeoDataFrame] = ()
223
145
  for gdf in self._gdfs:
224
146
  gdf = gdf.clip(mask)
147
+ collections = gdf.loc[gdf.geom_type == "GeometryCollection"]
148
+ if len(collections):
149
+ collections = collections.explode(index_parts=False)
150
+ gdf = pd.concat([gdf, collections], ignore_index=False)
225
151
  gdfs = gdfs + (gdf,)
226
152
  self._gdfs = gdfs
227
153
  self._gdf = pd.concat(gdfs, ignore_index=True)
228
154
  self._explore(**kwargs)
229
155
 
230
- def _update_column(self):
231
- self._is_categorical = self._check_if_categorical()
232
- self._fill_missings()
233
- self._gdf = pd.concat(self._gdfs, ignore_index=True)
234
-
235
156
  def _explore(self, **kwargs):
236
157
  self.kwargs = self.kwargs | kwargs
237
158
 
@@ -245,10 +166,46 @@ class Explore(Map):
245
166
  else:
246
167
  display(self.map)
247
168
 
169
+ def _split_categories(self):
170
+ new_gdfs, new_labels = [], []
171
+ for cat in self._unique_values:
172
+ gdf = self.gdf.loc[self.gdf[self.column] == cat]
173
+ new_gdfs.append(gdf)
174
+ new_labels.append(cat)
175
+ self._gdfs = new_gdfs
176
+ self._gdf = pd.concat(new_gdfs, ignore_index=True)
177
+ self.labels = new_labels
178
+
179
+ def _make_all_polygon(self):
180
+ """Buffer gdf if mixed geometry types
181
+
182
+ Because Folium does not handle GeometryCollection well
183
+ """
184
+
185
+ new_gdfs = []
186
+ for gdf in self.gdfs:
187
+ if get_geom_type(gdf) == "mixed":
188
+ gdf[gdf._geometry_column_name] = gdf.buffer(0.1)
189
+ new_gdfs.append(gdf)
190
+ self._gdfs = new_gdfs
191
+ self._gdf = pd.concat(new_gdfs, ignore_index=True)
192
+ self._nan_idx = self._gdf[self._column].isna()
193
+
194
+ def _update_column(self):
195
+ self._is_categorical = self._check_if_categorical()
196
+ self._fill_missings()
197
+ self._gdf = pd.concat(self._gdfs, ignore_index=True)
198
+
248
199
  def _create_categorical_map(self):
249
200
  self._get_categorical_colors()
250
201
 
251
- self.map = self._explore_return(self._gdf, return_="empty_map", **self.kwargs)
202
+ self.map = self._explore_return(
203
+ self._gdf,
204
+ return_="empty_map",
205
+ max_zoom=self.max_zoom,
206
+ popup=self.popup,
207
+ **self.kwargs,
208
+ )
252
209
 
253
210
  for gdf, label in zip(self._gdfs, self.labels, strict=True):
254
211
  if not len(gdf):
@@ -280,17 +237,22 @@ class Explore(Map):
280
237
 
281
238
  def _create_continous_map(self):
282
239
  self._prepare_continous_map()
283
- self.colorlist = self._get_continous_colors()
284
- self.colors = self._classify_from_bins(self._gdf)
240
+ if self.scheme:
241
+ classified = self._classify_from_bins(self._gdf, bins=self.bins)
242
+ classified_sequential = self._push_classification(classified)
243
+ n_colors = len(np.unique(classified_sequential)) - any(self._nan_idx)
244
+ unique_colors = self._get_continous_colors(n=n_colors)
285
245
 
286
246
  self.map = self._explore_return(
287
247
  self._gdf,
288
248
  return_="empty_map",
249
+ max_zoom=self.max_zoom,
250
+ popup=self.popup,
289
251
  **self.kwargs,
290
252
  )
291
253
 
292
254
  colorbar = bc.colormap.StepColormap(
293
- self.colorlist,
255
+ unique_colors,
294
256
  vmin=self._gdf[self._column].min(),
295
257
  vmax=self._gdf[self._column].max(),
296
258
  caption=self._column,
@@ -303,12 +265,13 @@ class Explore(Map):
303
265
  continue
304
266
  f = folium.FeatureGroup(name=label)
305
267
 
306
- self.colors = self._classify_from_bins(gdf)
268
+ classified = self._classify_from_bins(gdf, bins=self.bins)
269
+ colorarray = unique_colors[classified]
307
270
 
308
271
  gjs = self._explore_return(
309
272
  gdf,
310
273
  tooltip=self._tooltip_cols(gdf),
311
- color=self.colors,
274
+ color=colorarray,
312
275
  return_="geojson",
313
276
  **{key: value for key, value in self.kwargs.items() if key != "title"},
314
277
  )
@@ -415,7 +378,7 @@ class Explore(Map):
415
378
  if isinstance(tiles, xyzservices.TileProvider):
416
379
  attr = attr if attr else tiles.html_attribution
417
380
  map_kwds["min_zoom"] = tiles.get("min_zoom", 0)
418
- map_kwds["max_zoom"] = tiles.get("max_zoom", 18)
381
+ map_kwds["max_zoom"] = tiles.get("max_zoom", 30)
419
382
  tiles = tiles.build_url(scale_factor="{r}")
420
383
 
421
384
  m = folium.Map(