giga-spatial 0.6.4__py3-none-any.whl → 0.6.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.
@@ -1,4 +1,4 @@
1
- from typing import Dict, List, Optional, Union
1
+ from typing import Dict, List, Optional, Union, Literal
2
2
  from shapely.geometry import Polygon, MultiPolygon
3
3
 
4
4
  import geopandas as gpd
@@ -6,7 +6,6 @@ import pandas as pd
6
6
  import logging
7
7
 
8
8
  from gigaspatial.core.io.data_store import DataStore
9
- from gigaspatial.config import config as global_config
10
9
  from gigaspatial.processing.geo import (
11
10
  add_area_in_meters,
12
11
  get_centroids,
@@ -14,6 +13,7 @@ from gigaspatial.processing.geo import (
14
13
  from gigaspatial.handlers.ghsl import GHSLDataHandler
15
14
  from gigaspatial.handlers.google_open_buildings import GoogleOpenBuildingsHandler
16
15
  from gigaspatial.handlers.microsoft_global_buildings import MSBuildingsHandler
16
+ from gigaspatial.handlers.worldpop import WPPopulationHandler
17
17
  from gigaspatial.generators.zonal.base import (
18
18
  ZonalViewGenerator,
19
19
  ZonalViewGeneratorConfig,
@@ -136,9 +136,9 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
136
136
  List[T]: A list of zone identifiers in the order they appear in the
137
137
  underlying GeoDataFrame.
138
138
  """
139
- return self._zone_gdf[self.zone_id_column].tolist()
139
+ return self._zone_gdf.zone_id.tolist()
140
140
 
141
- def to_geodataframe(self) -> gpd.GeoDataFrame:
141
+ def get_zone_geodataframe(self) -> gpd.GeoDataFrame:
142
142
  """Convert zones to a GeoDataFrame with standardized column names.
143
143
 
144
144
  Returns:
@@ -156,24 +156,23 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
156
156
  year=2020,
157
157
  resolution=100,
158
158
  stat: str = "sum",
159
- name_prefix: str = "built_surface_m2_",
159
+ output_column: str = "built_surface_m2",
160
160
  **kwargs,
161
- ) -> gpd.GeoDataFrame:
161
+ ) -> pd.DataFrame:
162
162
  """Map GHSL Built-up Surface data to zones.
163
163
 
164
164
  Convenience method for mapping Global Human Settlement Layer Built-up Surface
165
165
  data using appropriate default parameters for built surface analysis.
166
166
 
167
167
  Args:
168
- ghsl_data_config (GHSLDataConfig): Configuration for GHSL Built-up Surface data.
169
- Defaults to GHS_BUILT_S product for 2020 at 100m resolution.
168
+ year: The year of the data (default: 2020)
169
+ resolution: The resolution in meters (default: 100)
170
170
  stat (str): Statistic to calculate for built surface values within each zone.
171
171
  Defaults to "sum" which gives total built surface area.
172
- name_prefix (str): Prefix for the output column name. Defaults to "built_surface_m2_".
173
-
172
+ output_column (str): The output column name. Defaults to "built_surface_m2".
174
173
  Returns:
175
- gpd.GeoDataFrame: Updated GeoDataFrame with zones and built surface metrics.
176
- Adds a column named "{name_prefix}{stat}" containing the aggregated values.
174
+ pd.DataFrame: Updated view DataFrame and settlement classification.
175
+ Adds a column with `output_column` containing the aggregated values.
177
176
  """
178
177
  handler = GHSLDataHandler(
179
178
  product="GHS_BUILT_S",
@@ -184,32 +183,31 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
184
183
  )
185
184
 
186
185
  return self.map_ghsl(
187
- handler=handler, stat=stat, name_prefix=name_prefix, **kwargs
186
+ handler=handler, stat=stat, output_column=output_column, **kwargs
188
187
  )
189
188
 
190
189
  def map_smod(
191
190
  self,
192
191
  year=2020,
193
- resolution=100,
192
+ resolution=1000,
194
193
  stat: str = "median",
195
- name_prefix: str = "smod_class_",
194
+ output_column: str = "smod_class",
196
195
  **kwargs,
197
- ) -> gpd.GeoDataFrame:
196
+ ) -> pd.DataFrame:
198
197
  """Map GHSL Settlement Model data to zones.
199
198
 
200
199
  Convenience method for mapping Global Human Settlement Layer Settlement Model
201
200
  data using appropriate default parameters for settlement classification analysis.
202
201
 
203
202
  Args:
204
- ghsl_data_config (GHSLDataConfig): Configuration for GHSL Settlement Model data.
205
- Defaults to GHS_SMOD product for 2020 at 1000m resolution in Mollweide projection.
203
+ year: The year of the data (default: 2020)
204
+ resolution: The resolution in meters (default: 1000)
206
205
  stat (str): Statistic to calculate for settlement class values within each zone.
207
206
  Defaults to "median" which gives the predominant settlement class.
208
- name_prefix (str): Prefix for the output column name. Defaults to "smod_class_".
209
-
207
+ output_column (str): The output column name. Defaults to "smod_class".
210
208
  Returns:
211
- gpd.GeoDataFrame: Updated GeoDataFrame with zones and settlement classification.
212
- Adds a column named "{name_prefix}{stat}" containing the aggregated values.
209
+ pd.DataFrame: Updated view DataFrame and settlement classification.
210
+ Adds a column with `output_column` containing the aggregated values.
213
211
  """
214
212
  handler = GHSLDataHandler(
215
213
  product="GHS_SMOD",
@@ -221,32 +219,31 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
221
219
  )
222
220
 
223
221
  return self.map_ghsl(
224
- handler=handler, stat=stat, name_prefix=name_prefix, **kwargs
222
+ handler=handler, stat=stat, output_column=output_column, **kwargs
225
223
  )
226
224
 
227
225
  def map_ghsl(
228
226
  self,
229
227
  handler: GHSLDataHandler,
230
228
  stat: str,
231
- name_prefix: Optional[str] = None,
229
+ output_column: Optional[str] = None,
232
230
  **kwargs,
233
- ) -> gpd.GeoDataFrame:
231
+ ) -> pd.DataFrame:
234
232
  """Map Global Human Settlement Layer data to zones.
235
233
 
236
234
  Loads and processes GHSL raster data for the intersecting tiles, then samples
237
235
  the raster values within each zone using the specified statistic.
238
236
 
239
237
  Args:
240
- ghsl_data_config (GHSLDataConfig): Configuration specifying which GHSL
241
- product, year, resolution, and coordinate system to use.
238
+ hander (GHSLDataHandler): Handler for the GHSL data.
242
239
  stat (str): Statistic to calculate for raster values within each zone.
243
240
  Common options: "mean", "sum", "median", "min", "max".
244
- name_prefix (str, optional): Prefix for the output column name.
241
+ output_column (str): The output column name.
245
242
  If None, uses the GHSL product name in lowercase followed by underscore.
246
243
 
247
244
  Returns:
248
- gpd.GeoDataFrame: Updated GeoDataFrame with zones and GHSL metrics.
249
- Adds a column named "{name_prefix}{stat}" containing the sampled values.
245
+ pd.DataFrame: Updated DataFrame with GHSL metrics.
246
+ Adds a column named as `output_column` containing the sampled values.
250
247
 
251
248
  Note:
252
249
  The method automatically determines which GHSL tiles intersect with the zones
@@ -265,21 +262,21 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
265
262
  )
266
263
  sampled_values = self.map_rasters(tif_processors=tif_processors, stat=stat)
267
264
 
268
- name_prefix = (
269
- name_prefix if name_prefix else handler.config.product.lower() + "_"
265
+ column_name = (
266
+ output_column
267
+ if output_column
268
+ else f"{handler.config.product.lower()}_{stat}"
270
269
  )
271
- column_name = f"{name_prefix}{stat}"
272
- self._zone_gdf[column_name] = sampled_values
273
270
 
274
- self.logger.info(f"Added {column_name} column")
271
+ self.add_variable_to_view(sampled_values, column_name)
275
272
 
276
- return self._zone_gdf.copy()
273
+ return self.view
277
274
 
278
275
  def map_google_buildings(
279
276
  self,
280
277
  handler: Optional[GoogleOpenBuildingsHandler] = None,
281
278
  use_polygons: bool = False,
282
- ) -> gpd.GeoDataFrame:
279
+ ) -> pd.DataFrame:
283
280
  """Map Google Open Buildings data to zones.
284
281
 
285
282
  Processes Google Open Buildings dataset to calculate building counts and total
@@ -295,7 +292,7 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
295
292
  area values from attributes for faster processing. Defaults to False.
296
293
 
297
294
  Returns:
298
- gpd.GeoDataFrame: Updated GeoDataFrame with zones and building metrics.
295
+ pd.DataFrame: Updated DataFrame with building metrics.
299
296
  Adds columns:
300
297
  - 'google_buildings_count': Number of buildings in each zone
301
298
  - 'google_buildings_area_in_meters': Total building area in square meters
@@ -341,19 +338,20 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
341
338
  self.logger.info(
342
339
  "Calculating building areas with area-weighted aggregation"
343
340
  )
344
- area_result = self.map_polygons(buildings_gdf, area_weighted=True)
341
+ area_result = self.map_polygons(
342
+ buildings_gdf,
343
+ value_columns="area_in_meters",
344
+ aggregation="sum",
345
+ predicate="fractional",
346
+ )
345
347
 
346
348
  self.logger.info("Counting buildings using points data")
347
349
  count_result = self.map_points(points=buildings_df, predicate="within")
348
350
 
349
- self._zone_gdf["google_buildings_count"] = self.zone_gdf.index.map(count_result)
350
- self._zone_gdf["google_buildings_area_in_meters"] = self.zone_gdf.index.map(
351
- area_result
352
- )
353
-
354
- self.logger.info(f"Added Google building data")
351
+ self.add_variable_to_view(count_result, "google_buildings_count")
352
+ self.add_variable_to_view(area_result, "google_buildings_area_in_meters")
355
353
 
356
- return self._zone_gdf.copy()
354
+ return self.view
357
355
 
358
356
  def map_ms_buildings(
359
357
  self,
@@ -400,7 +398,9 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
400
398
  )
401
399
  return self._zone_gdf.copy()
402
400
 
403
- buildings_gdf = add_area_in_meters(buildings_gdf)
401
+ buildings_gdf = add_area_in_meters(
402
+ buildings_gdf, area_column_name="area_in_meters"
403
+ )
404
404
 
405
405
  building_centroids = get_centroids(buildings_gdf)
406
406
 
@@ -421,7 +421,12 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
421
421
  self.logger.info(
422
422
  "Calculating building areas with area-weighted aggregation"
423
423
  )
424
- area_result = self.map_polygons(buildings_gdf, area_weighted=True)
424
+ area_result = self.map_polygons(
425
+ buildings_gdf,
426
+ value_columns="area_in_meters",
427
+ aggregation="sum",
428
+ predicate="fractional",
429
+ )
425
430
 
426
431
  self.logger.info("Counting Microsoft buildings per zone")
427
432
 
@@ -429,11 +434,104 @@ class GeometryBasedZonalViewGenerator(ZonalViewGenerator[T]):
429
434
  points=building_centroids, predicate="within"
430
435
  )
431
436
 
432
- self._zone_gdf["ms_buildings_count"] = self.zone_gdf.index.map(count_result)
433
- self._zone_gdf["ms_buildings_area_in_meters"] = self.zone_gdf.index.map(
434
- area_result
437
+ self.add_variable_to_view(count_result, "ms_buildings_count")
438
+ self.add_variable_to_view(area_result, "ms_buildings_area_in_meters")
439
+
440
+ return self.view
441
+
442
+ def map_ghsl_pop(
443
+ self,
444
+ resolution=100,
445
+ stat: str = "sum",
446
+ output_column: str = "ghsl_pop",
447
+ predicate: Literal["intersects", "fractional"] = "intersects",
448
+ **kwargs,
449
+ ):
450
+ handler = GHSLDataHandler(
451
+ product="GHS_POP",
452
+ resolution=resolution,
453
+ data_store=self.data_store,
454
+ **kwargs,
435
455
  )
436
456
 
437
- self.logger.info(f"Added Microsoft building data")
457
+ if predicate == "fractional":
458
+ if resolution == 100:
459
+ self.logger.warning(
460
+ "Fractional aggregations only supported for datasets with 1000m resolution. Using `intersects` as predicate"
461
+ )
462
+ predicate = "intersects"
463
+ else:
464
+ gdf_pop = handler.load_into_geodataframe(self.zone_gdf)
465
+
466
+ result = self.map_polygons(
467
+ gdf_pop,
468
+ value_columns="pixel_value",
469
+ aggregation="sum",
470
+ predicate="fractional",
471
+ )
472
+
473
+ self.add_variable_to_view(result, output_column)
474
+ return self.view
475
+
476
+ return self.map_ghsl(
477
+ handler=handler, stat=stat, output_column=output_column, **kwargs
478
+ )
479
+
480
+ def map_wp_pop(
481
+ self,
482
+ country: Union[str, List[str]],
483
+ resolution=1000,
484
+ predicate: Literal["intersects", "fractional"] = "intersects",
485
+ output_column: str = "population",
486
+ **kwargs,
487
+ ):
488
+ if isinstance(country, str):
489
+ country = [country]
490
+
491
+ handler = WPPopulationHandler(
492
+ project="pop", resolution=resolution, data_store=self.data_store, **kwargs
493
+ )
494
+
495
+ self.logger.info(
496
+ f"Mapping WorldPop Population data (year: {handler.config.year}, resolution: {handler.config.resolution}m)"
497
+ )
498
+
499
+ if predicate == "fractional":
500
+ if resolution == 100:
501
+ self.logger.warning(
502
+ "Fractional aggregations only supported for datasets with 1000m resolution. Using `intersects` as predicate"
503
+ )
504
+ predicate = "intersects"
505
+ else:
506
+ gdf_pop = pd.concat(
507
+ [
508
+ handler.load_into_geodataframe(
509
+ c, ensure_available=self.config.ensure_available
510
+ )
511
+ for c in country
512
+ ],
513
+ ignore_index=True,
514
+ )
515
+
516
+ result = self.map_polygons(
517
+ gdf_pop,
518
+ value_columns="pixel_value",
519
+ aggregation="sum",
520
+ predicate=predicate,
521
+ )
522
+
523
+ self.add_variable_to_view(result, output_column)
524
+ return self.view
525
+
526
+ tif_processors = []
527
+ for c in country:
528
+ tif_processors.extend(
529
+ handler.load_data(c, ensure_available=self.config.ensure_available)
530
+ )
531
+
532
+ self.logger.info(f"Sampling WorldPop Population data using 'sum' statistic")
533
+ sampled_values = self.map_rasters(tif_processors=tif_processors, stat="sum")
534
+
535
+ self.add_variable_to_view(sampled_values, output_column)
438
536
 
439
- return self._zone_gdf.copy()
537
+ return self.view
@@ -15,25 +15,48 @@ from gigaspatial.generators.zonal.geometry import GeometryBasedZonalViewGenerato
15
15
 
16
16
 
17
17
  class MercatorViewGenerator(GeometryBasedZonalViewGenerator[T]):
18
- """Mid-level class for zonal view generation based on geometries with identifiers.
18
+ """
19
+ Generates zonal views using Mercator tiles as the zones.
19
20
 
20
- This class serves as an intermediate between the abstract ZonalViewGenerator and specific
21
- implementations like MercatorViewGenerator or H3ViewGenerator. It handles the common case
22
- where zones are defined by a mapping between zone identifiers and geometries, either
23
- provided as a dictionary or as a GeoDataFrame.
21
+ This class specializes in creating zonal views where the zones are defined by
22
+ Mercator tiles. It extends the `GeometryBasedZonalViewGenerator` and leverages
23
+ the `MercatorTiles` and `CountryMercatorTiles` classes to generate tiles based on
24
+ various input sources.
24
25
 
25
- The class extends the base functionality with methods for mapping common geospatial
26
- datasets including GHSL (Global Human Settlement Layer), Google Open Buildings,
27
- and Microsoft Global Buildings data.
26
+ The primary input source defines the geographical area of interest. This can be
27
+ a country, a specific geometry, a set of points, or even a list of predefined
28
+ quadkeys. The `zoom_level` determines the granularity of the Mercator tiles.
28
29
 
29
30
  Attributes:
30
- zone_dict (Dict[T, Polygon]): Mapping of zone identifiers to geometries.
31
- zone_id_column (str): Name of the column containing zone identifiers.
32
- zone_data_crs (str): Coordinate reference system of the zone data.
33
- _zone_gdf (gpd.GeoDataFrame): Cached GeoDataFrame representation of zones.
34
- data_store (DataStore): For accessing input data.
35
- generator_config (ZonalViewGeneratorConfig): Configuration for view generation.
36
- logger: Logger instance for this class.
31
+ source (Union[str, BaseGeometry, gpd.GeoDataFrame, List[Union[Point, Tuple[float, float]]], List[str]]):
32
+ Specifies the geographic area or specific tiles to use. Can be:
33
+ - A country name (str): Uses `CountryMercatorTiles` to generate tiles covering the country.
34
+ - A Shapely geometry (BaseGeometry): Uses `MercatorTiles.from_spatial` to create tiles intersecting the geometry.
35
+ - A GeoDataFrame (gpd.GeoDataFrame): Uses `MercatorTiles.from_spatial` to create tiles intersecting the geometries.
36
+ - A list of points (List[Union[Point, Tuple[float, float]]]): Uses `MercatorTiles.from_spatial` to create tiles containing the points.
37
+ - A list of quadkeys (List[str]): Uses `MercatorTiles.from_quadkeys` to use the specified tiles directly.
38
+ zoom_level (int): The zoom level of the Mercator tiles. Higher zoom levels result in smaller, more detailed tiles.
39
+ predicate (str): The spatial predicate used when filtering tiles based on a spatial source (e.g., "intersects", "contains"). Defaults to "intersects".
40
+ config (Optional[ZonalViewGeneratorConfig]): Configuration for the zonal view generation process.
41
+ data_store (Optional[DataStore]): A DataStore instance for accessing data.
42
+ logger (Optional[logging.Logger]): A logger instance for logging.
43
+
44
+ Methods:
45
+ _init_zone_data(source, zoom_level, predicate): Initializes the Mercator tile GeoDataFrame based on the input source.
46
+ # Inherits other methods from GeometryBasedZonalViewGenerator, such as:
47
+ # map_ghsl(), map_google_buildings(), map_ms_buildings(), aggregate_data(), save_view()
48
+
49
+ Example:
50
+ # Create a MercatorViewGenerator for tiles covering Germany at zoom level 6
51
+ generator = MercatorViewGenerator(source="Germany", zoom_level=6)
52
+
53
+ # Create a MercatorViewGenerator for tiles intersecting a specific polygon
54
+ polygon = ... # Define a Shapely Polygon
55
+ generator = MercatorViewGenerator(source=polygon, zoom_level=8)
56
+
57
+ # Create a MercatorViewGenerator from a list of quadkeys
58
+ quadkeys = ["0020023131023032", "0020023131023033"]
59
+ generator = MercatorViewGenerator(source=quadkeys, zoom_level=12)
37
60
  """
38
61
 
39
62
  def __init__(
@@ -53,16 +76,19 @@ class MercatorViewGenerator(GeometryBasedZonalViewGenerator[T]):
53
76
  ):
54
77
 
55
78
  super().__init__(
56
- zone_data=self._init_zone_data(source, zoom_level, predicate),
79
+ zone_data=self._init_zone_data(source, zoom_level, predicate, data_store),
57
80
  zone_id_column="quadkey",
58
81
  config=config,
59
82
  data_store=data_store,
60
83
  logger=logger,
61
84
  )
85
+ self.logger.info(f"Initialized MercatorViewGenerator")
62
86
 
63
- def _init_zone_data(self, source, zoom_level, predicate):
87
+ def _init_zone_data(self, source, zoom_level, predicate, data_store=None):
64
88
  if isinstance(source, str):
65
- tiles = CountryMercatorTiles.create(country=source, zoom_level=zoom_level)
89
+ tiles = CountryMercatorTiles.create(
90
+ country=source, zoom_level=zoom_level, data_store=data_store
91
+ )
66
92
  elif isinstance(source, (BaseGeometry, Iterable)):
67
93
  if isinstance(source, Iterable) and all(
68
94
  isinstance(qk, str) for qk in source
@@ -73,6 +99,11 @@ class MercatorViewGenerator(GeometryBasedZonalViewGenerator[T]):
73
99
  source=source, zoom_level=zoom_level, predicate=predicate
74
100
  )
75
101
  else:
76
- raise ValueError("sadadasfasfkasmf")
102
+ raise TypeError(
103
+ f"Unsupported source type for MercatorViewGenerator. 'source' must be "
104
+ f"a country name (str), a Shapely geometry, a GeoDataFrame, "
105
+ f"a list of quadkeys (str), or a list of (lon, lat) tuples/Shapely Point objects. "
106
+ f"Received type: {type(source)}."
107
+ )
77
108
 
78
109
  return tiles.to_geodataframe()
@@ -1 +1 @@
1
- from gigaspatial.grid.mercator_tiles import *
1
+ from gigaspatial.grid.mercator_tiles import MercatorTiles, CountryMercatorTiles
@@ -4,10 +4,10 @@ import mercantile
4
4
  from shapely.geometry import box
5
5
  from shapely.geometry.base import BaseGeometry
6
6
  from shapely.strtree import STRtree
7
- from shapely import MultiPolygon, Polygon, Point
7
+ from shapely import Point
8
8
  import json
9
9
  from pathlib import Path
10
- from pydantic import BaseModel, Field, PrivateAttr
10
+ from pydantic import BaseModel, Field
11
11
  from typing import List, Union, Iterable, Optional, Tuple, ClassVar
12
12
  import pycountry
13
13
 
@@ -31,6 +31,9 @@ class MercatorTiles(BaseModel):
31
31
  if not quadkeys:
32
32
  cls.logger.warning("No quadkeys provided to from_quadkeys.")
33
33
  return cls(zoom_level=0, quadkeys=[])
34
+ cls.logger.info(
35
+ f"Initializing MercatorTiles from {len(quadkeys)} provided quadkeys."
36
+ )
34
37
  return cls(zoom_level=len(quadkeys[0]), quadkeys=set(quadkeys))
35
38
 
36
39
  @classmethod
@@ -120,14 +123,7 @@ class MercatorTiles(BaseModel):
120
123
  cls.logger.info(
121
124
  f"Creating MercatorTiles from {len(points)} points at zoom level: {zoom_level}"
122
125
  )
123
- quadkeys = {
124
- (
125
- mercantile.quadkey(mercantile.tile(p.x, p.y, zoom_level))
126
- if isinstance(p, Point)
127
- else mercantile.quadkey(mercantile.tile(p[1], p[0], zoom_level))
128
- )
129
- for p in points
130
- }
126
+ quadkeys = set(cls.get_quadkeys_from_points(points, zoom_level))
131
127
  cls.logger.info(f"Generated {len(quadkeys)} unique quadkeys from points.")
132
128
  return cls(zoom_level=zoom_level, quadkeys=list(quadkeys), **kwargs)
133
129
 
@@ -219,6 +215,29 @@ class MercatorTiles(BaseModel):
219
215
  {"quadkey": self.quadkeys, "geometry": self.to_geoms()}, crs="EPSG:4326"
220
216
  )
221
217
 
218
+ @staticmethod
219
+ def get_quadkeys_from_points(
220
+ points: List[Union[Point, Tuple[float, float]]], zoom_level: int
221
+ ) -> List[str]:
222
+ """Get list of quadkeys for the provided points at specified zoom level.
223
+
224
+ Args:
225
+ points: List of points as either shapely Points or (lon, lat) tuples
226
+ zoom_level: Zoom level for the quadkeys
227
+
228
+ Returns:
229
+ List of quadkey strings
230
+ """
231
+ quadkeys = [
232
+ (
233
+ mercantile.quadkey(mercantile.tile(p.x, p.y, zoom_level))
234
+ if isinstance(p, Point)
235
+ else mercantile.quadkey(mercantile.tile(p[1], p[0], zoom_level))
236
+ )
237
+ for p in points
238
+ ]
239
+ return quadkeys
240
+
222
241
  def save(self, file: Union[str, Path], format: str = "json") -> None:
223
242
  """Save MercatorTiles to file in specified format."""
224
243
  with self.data_store.open(str(file), "wb" if format == "parquet" else "w") as f:
@@ -270,6 +289,10 @@ class CountryMercatorTiles(MercatorTiles):
270
289
  country=pycountry.countries.lookup(country).alpha_3,
271
290
  )
272
291
 
292
+ cls.logger.info(
293
+ f"Initializing Mercator zones for country: {country} at zoom level {zoom_level}"
294
+ )
295
+
273
296
  country_geom = (
274
297
  AdminBoundaries.create(
275
298
  country_code=country,
@@ -21,7 +21,14 @@ from gigaspatial.handlers.osm import OSMLocationFetcher
21
21
  from gigaspatial.handlers.overture import OvertureAmenityFetcher
22
22
  from gigaspatial.handlers.mapbox_image import MapboxImageDownloader
23
23
  from gigaspatial.handlers.maxar_image import MaxarConfig, MaxarImageDownloader
24
- from gigaspatial.handlers.worldpop import WorldPopConfig, WorldPopDownloader
24
+
25
+ from gigaspatial.handlers.worldpop import (
26
+ WPPopulationConfig,
27
+ WPPopulationReader,
28
+ WPPopulationDownloader,
29
+ WPPopulationHandler,
30
+ WorldPopRestClient,
31
+ )
25
32
  from gigaspatial.handlers.ookla_speedtest import (
26
33
  OoklaSpeedtestTileConfig,
27
34
  OoklaSpeedtestConfig,
@@ -1,4 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass, field
2
3
  from pathlib import Path
3
4
  from typing import Any, List, Optional, Union, Tuple, Callable, Iterable
4
5
  import pandas as pd
@@ -13,7 +14,6 @@ from gigaspatial.core.io.data_store import DataStore
13
14
  from gigaspatial.core.io.local_data_store import LocalDataStore
14
15
  from gigaspatial.core.io.readers import read_dataset
15
16
  from gigaspatial.processing.tif_processor import TifProcessor
16
- from dataclasses import dataclass, field
17
17
 
18
18
 
19
19
  @dataclass
@@ -584,6 +584,8 @@ class BaseHandler(ABC):
584
584
  bool: True if data is available after this operation
585
585
  """
586
586
  try:
587
+ data_units = None
588
+ data_paths = None
587
589
  # Resolve what data units are needed
588
590
  if hasattr(self.config, "get_relevant_data_units"):
589
591
  data_units = self.config.get_relevant_data_units(source, **kwargs)
@@ -606,11 +608,29 @@ class BaseHandler(ABC):
606
608
  if not missing_paths:
607
609
  self.logger.info("All required data is already available")
608
610
  return True
609
-
610
- # Download missing or all data
611
- if hasattr(self.config, "get_relevant_data_units"):
612
- data_units = self.config.get_relevant_data_units(source, **kwargs)
613
- self.downloader.download_data_units(data_units, **kwargs)
611
+ else:
612
+ # If force_download, treat all as missing
613
+ missing_paths = data_paths
614
+
615
+ if not missing_paths:
616
+ self.logger.info("No missing data to download.")
617
+ return True
618
+
619
+ # Download logic
620
+ if data_units is not None:
621
+ # Map data_units to their paths and select only those that are missing
622
+ unit_to_path = dict(zip(data_units, data_paths))
623
+ if force_download:
624
+ # Download all units if force_download
625
+ self.downloader.download_data_units(data_units, **kwargs)
626
+ else:
627
+ missing_units = [
628
+ unit
629
+ for unit, path in unit_to_path.items()
630
+ if path in missing_paths
631
+ ]
632
+ if missing_units:
633
+ self.downloader.download_data_units(missing_units, **kwargs)
614
634
  else:
615
635
  self.downloader.download(source, **kwargs)
616
636