giga-spatial 0.6.0__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.
- giga_spatial-0.6.0.dist-info/METADATA +141 -0
- giga_spatial-0.6.0.dist-info/RECORD +47 -0
- giga_spatial-0.6.0.dist-info/WHEEL +5 -0
- giga_spatial-0.6.0.dist-info/licenses/LICENSE +661 -0
- giga_spatial-0.6.0.dist-info/top_level.txt +1 -0
- gigaspatial/__init__.py +1 -0
- gigaspatial/config.py +226 -0
- gigaspatial/core/__init__.py +0 -0
- gigaspatial/core/io/__init__.py +5 -0
- gigaspatial/core/io/adls_data_store.py +325 -0
- gigaspatial/core/io/data_api.py +113 -0
- gigaspatial/core/io/data_store.py +147 -0
- gigaspatial/core/io/local_data_store.py +92 -0
- gigaspatial/core/io/readers.py +265 -0
- gigaspatial/core/io/writers.py +128 -0
- gigaspatial/core/schemas/__init__.py +0 -0
- gigaspatial/core/schemas/entity.py +244 -0
- gigaspatial/generators/__init__.py +2 -0
- gigaspatial/generators/poi.py +636 -0
- gigaspatial/generators/zonal/__init__.py +3 -0
- gigaspatial/generators/zonal/base.py +370 -0
- gigaspatial/generators/zonal/geometry.py +439 -0
- gigaspatial/generators/zonal/mercator.py +78 -0
- gigaspatial/grid/__init__.py +1 -0
- gigaspatial/grid/mercator_tiles.py +286 -0
- gigaspatial/handlers/__init__.py +40 -0
- gigaspatial/handlers/base.py +761 -0
- gigaspatial/handlers/boundaries.py +305 -0
- gigaspatial/handlers/ghsl.py +772 -0
- gigaspatial/handlers/giga.py +145 -0
- gigaspatial/handlers/google_open_buildings.py +472 -0
- gigaspatial/handlers/hdx.py +241 -0
- gigaspatial/handlers/mapbox_image.py +208 -0
- gigaspatial/handlers/maxar_image.py +291 -0
- gigaspatial/handlers/microsoft_global_buildings.py +548 -0
- gigaspatial/handlers/ookla_speedtest.py +199 -0
- gigaspatial/handlers/opencellid.py +290 -0
- gigaspatial/handlers/osm.py +356 -0
- gigaspatial/handlers/overture.py +126 -0
- gigaspatial/handlers/rwi.py +157 -0
- gigaspatial/handlers/unicef_georepo.py +806 -0
- gigaspatial/handlers/worldpop.py +266 -0
- gigaspatial/processing/__init__.py +4 -0
- gigaspatial/processing/geo.py +1054 -0
- gigaspatial/processing/sat_images.py +39 -0
- gigaspatial/processing/tif_processor.py +477 -0
- gigaspatial/processing/utils.py +49 -0
@@ -0,0 +1,291 @@
|
|
1
|
+
from typing import Optional, Tuple, List, Union, Any, Literal
|
2
|
+
from pydantic import BaseModel, Field, HttpUrl, field_validator
|
3
|
+
from pathlib import Path
|
4
|
+
import mercantile
|
5
|
+
from tqdm import tqdm
|
6
|
+
import geopandas as gpd
|
7
|
+
import pandas as pd
|
8
|
+
from owslib.wms import WebMapService
|
9
|
+
from time import sleep
|
10
|
+
from gigaspatial.grid.mercator_tiles import MercatorTiles
|
11
|
+
from gigaspatial.core.io.data_store import DataStore
|
12
|
+
from gigaspatial.core.io.local_data_store import LocalDataStore
|
13
|
+
from gigaspatial.processing.geo import (
|
14
|
+
convert_to_geodataframe,
|
15
|
+
buffer_geodataframe,
|
16
|
+
)
|
17
|
+
from gigaspatial.processing.sat_images import calculate_pixels_at_location
|
18
|
+
from gigaspatial.config import config as global_config
|
19
|
+
|
20
|
+
|
21
|
+
class MaxarConfig(BaseModel):
|
22
|
+
"""Configuration for Maxar Image Downloader using Pydantic"""
|
23
|
+
|
24
|
+
username: str = Field(
|
25
|
+
default=global_config.MAXAR_USERNAME, description="Maxar API username"
|
26
|
+
)
|
27
|
+
password: str = Field(
|
28
|
+
default=global_config.MAXAR_PASSWORD, description="Maxar API password"
|
29
|
+
)
|
30
|
+
connection_string: str = Field(
|
31
|
+
default=global_config.MAXAR_CONNECTION_STRING,
|
32
|
+
description="Maxar WMS connection string",
|
33
|
+
)
|
34
|
+
|
35
|
+
base_url: HttpUrl = Field(
|
36
|
+
default="https://evwhs.digitalglobe.com/mapservice/wmsaccess?",
|
37
|
+
description="Base URL for Maxar WMS service",
|
38
|
+
)
|
39
|
+
|
40
|
+
layers: List[Literal["DigitalGlobe:ImageryFootprint", "DigitalGlobe:Imagery"]] = (
|
41
|
+
Field(
|
42
|
+
default=["DigitalGlobe:Imagery"],
|
43
|
+
description="List of layers to request from WMS",
|
44
|
+
)
|
45
|
+
)
|
46
|
+
|
47
|
+
feature_profile: str = Field(
|
48
|
+
default="Most_Aesthetic_Mosaic_Profile",
|
49
|
+
description="Feature profile to use for WMS requests",
|
50
|
+
)
|
51
|
+
|
52
|
+
coverage_cql_filter: str = Field(
|
53
|
+
default="", description="CQL filter for coverage selection"
|
54
|
+
)
|
55
|
+
|
56
|
+
exceptions: str = Field(
|
57
|
+
default="application/vnd.ogc.se_xml",
|
58
|
+
description="Exception handling format for WMS",
|
59
|
+
)
|
60
|
+
|
61
|
+
transparent: bool = Field(
|
62
|
+
default=True,
|
63
|
+
description="Whether the requested images should have transparency",
|
64
|
+
)
|
65
|
+
|
66
|
+
image_format: Literal["image/png", "image/jpeg", "image/geotiff"] = Field(
|
67
|
+
default="image/png",
|
68
|
+
)
|
69
|
+
|
70
|
+
data_crs: Literal["EPSG:4326", "EPSG:3395", "EPSG:3857", "CAR:42004"] = Field(
|
71
|
+
default="EPSG:4326"
|
72
|
+
)
|
73
|
+
|
74
|
+
max_retries: int = Field(
|
75
|
+
default=3, description="Number of retries for failed image downloads"
|
76
|
+
)
|
77
|
+
|
78
|
+
retry_delay: int = Field(default=5, description="Delay in seconds between retries")
|
79
|
+
|
80
|
+
@field_validator("username", "password", "connection_string")
|
81
|
+
@classmethod
|
82
|
+
def validate_non_empty(cls, value: str, field) -> str:
|
83
|
+
"""Ensure required credentials are provided"""
|
84
|
+
if not value or value.strip() == "":
|
85
|
+
raise ValueError(
|
86
|
+
f"{field.name} cannot be empty. Please provide a valid {field.name}."
|
87
|
+
)
|
88
|
+
return value
|
89
|
+
|
90
|
+
@property
|
91
|
+
def wms_url(self) -> str:
|
92
|
+
"""Generate the full WMS URL with connection string"""
|
93
|
+
return f"{self.base_url}connectid={self.connection_string}"
|
94
|
+
|
95
|
+
@property
|
96
|
+
def suffix(self) -> str:
|
97
|
+
return f".{self.image_format.split('/')[1]}"
|
98
|
+
|
99
|
+
|
100
|
+
class MaxarImageDownloader:
|
101
|
+
"""Class to download images from Maxar"""
|
102
|
+
|
103
|
+
def __init__(
|
104
|
+
self,
|
105
|
+
config: Optional[MaxarConfig] = None,
|
106
|
+
data_store: Optional[DataStore] = None,
|
107
|
+
):
|
108
|
+
"""
|
109
|
+
Initialize the downloader with Maxar config.
|
110
|
+
|
111
|
+
Args:
|
112
|
+
config: MaxarConfig instance containing credentials and settings
|
113
|
+
data_store: Instance of DataStore for accessing data storage
|
114
|
+
"""
|
115
|
+
self.config = config or MaxarConfig()
|
116
|
+
self.wms = WebMapService(
|
117
|
+
self.config.wms_url,
|
118
|
+
username=self.config.username,
|
119
|
+
password=self.config.password,
|
120
|
+
)
|
121
|
+
self.data_store = data_store or LocalDataStore()
|
122
|
+
self.logger = global_config.get_logger(self.__class__.__name__)
|
123
|
+
|
124
|
+
def _download_single_image(self, bbox, output_path: Union[Path, str], size) -> bool:
|
125
|
+
"""Download a single image from bbox and pixel size"""
|
126
|
+
for attempt in range(self.config.max_retries):
|
127
|
+
try:
|
128
|
+
img_data = self.wms.getmap(
|
129
|
+
bbox=bbox,
|
130
|
+
layers=self.config.layers,
|
131
|
+
srs=self.config.data_crs,
|
132
|
+
size=size,
|
133
|
+
featureProfile=self.config.feature_profile,
|
134
|
+
coverage_cql_filter=self.config.coverage_cql_filter,
|
135
|
+
exceptions=self.config.exceptions,
|
136
|
+
transparent=self.config.transparent,
|
137
|
+
format=self.config.image_format,
|
138
|
+
)
|
139
|
+
self.data_store.write_file(str(output_path), img_data.read())
|
140
|
+
return True
|
141
|
+
except Exception as e:
|
142
|
+
self.logger.warning(
|
143
|
+
f"Attempt {attempt + 1} of downloading {output_path.name} failed: {str(e)}"
|
144
|
+
)
|
145
|
+
if attempt < self.max_retries - 1:
|
146
|
+
sleep(self.config.retry_delay)
|
147
|
+
else:
|
148
|
+
self.logger.warning(
|
149
|
+
f"Failed to download {output_path.name} after {self.config.max_retries} attemps: {str(e)}"
|
150
|
+
)
|
151
|
+
return False
|
152
|
+
|
153
|
+
def download_images_by_tiles(
|
154
|
+
self,
|
155
|
+
mercator_tiles: "MercatorTiles",
|
156
|
+
output_dir: Union[str, Path],
|
157
|
+
image_size: Tuple[int, int] = (512, 512),
|
158
|
+
image_prefix: str = "maxar_image_",
|
159
|
+
) -> None:
|
160
|
+
"""
|
161
|
+
Download images for given mercator tiles using the specified style
|
162
|
+
|
163
|
+
Args:
|
164
|
+
mercator_tiles: MercatorTiles instance containing quadkeys
|
165
|
+
output_dir: Directory to save images
|
166
|
+
image_size: Tuple of (width, height) for output images
|
167
|
+
image_prefix: Prefix for output image names
|
168
|
+
"""
|
169
|
+
output_dir = Path(output_dir)
|
170
|
+
|
171
|
+
image_size_str = f"{image_size[0]}x{image_size[1]}"
|
172
|
+
total_tiles = len(mercator_tiles.quadkeys)
|
173
|
+
|
174
|
+
self.logger.info(
|
175
|
+
f"Downloading {total_tiles} tiles with size {image_size_str}..."
|
176
|
+
)
|
177
|
+
|
178
|
+
def _get_tile_bounds(quadkey: str) -> Tuple[float]:
|
179
|
+
"""Get tile bounds from quadkey"""
|
180
|
+
tile = mercantile.quadkey_to_tile(quadkey)
|
181
|
+
bounds = mercantile.bounds(tile)
|
182
|
+
return (bounds.west, bounds.south, bounds.east, bounds.north)
|
183
|
+
|
184
|
+
def download_image(
|
185
|
+
quadkey: str, image_size: Tuple[int, int], suffix: str = self.config.suffix
|
186
|
+
) -> bool:
|
187
|
+
bounds = _get_tile_bounds(quadkey)
|
188
|
+
file_name = f"{image_prefix}{quadkey}{suffix}"
|
189
|
+
|
190
|
+
success = self._download_single_image(
|
191
|
+
bounds, output_dir / file_name, image_size
|
192
|
+
)
|
193
|
+
|
194
|
+
return success
|
195
|
+
|
196
|
+
successful_downloads = 0
|
197
|
+
with tqdm(total=total_tiles) as pbar:
|
198
|
+
for quadkey in mercator_tiles.quadkeys:
|
199
|
+
if download_image(quadkey, image_size):
|
200
|
+
successful_downloads += 1
|
201
|
+
pbar.update(1)
|
202
|
+
|
203
|
+
self.logger.info(
|
204
|
+
f"Successfully downloaded {successful_downloads}/{total_tiles} images!"
|
205
|
+
)
|
206
|
+
|
207
|
+
def download_images_by_bounds(
|
208
|
+
self,
|
209
|
+
gdf: gpd.GeoDataFrame,
|
210
|
+
output_dir: Union[str, Path],
|
211
|
+
image_size: Tuple[int, int] = (512, 512),
|
212
|
+
image_prefix: str = "maxar_image_",
|
213
|
+
) -> None:
|
214
|
+
"""
|
215
|
+
Download images for given points using the specified style
|
216
|
+
|
217
|
+
Args:
|
218
|
+
gdf_points: GeoDataFrame containing bounding box polygons
|
219
|
+
output_dir: Directory to save images
|
220
|
+
image_size: Tuple of (width, height) for output images
|
221
|
+
image_prefix: Prefix for output image names
|
222
|
+
"""
|
223
|
+
output_dir = Path(output_dir)
|
224
|
+
|
225
|
+
image_size_str = f"{image_size[0]}x{image_size[1]}"
|
226
|
+
total_images = len(gdf)
|
227
|
+
|
228
|
+
self.logger.info(
|
229
|
+
f"Downloading {total_images} images with size {image_size_str}..."
|
230
|
+
)
|
231
|
+
|
232
|
+
def download_image(
|
233
|
+
idx: Any,
|
234
|
+
bounds: Tuple[float, float, float, float],
|
235
|
+
image_size,
|
236
|
+
suffix: str = self.config.suffix,
|
237
|
+
) -> bool:
|
238
|
+
file_name = f"{image_prefix}{idx}{suffix}"
|
239
|
+
success = self._download_single_image(
|
240
|
+
bounds, output_dir / file_name, image_size
|
241
|
+
)
|
242
|
+
return success
|
243
|
+
|
244
|
+
gdf = gdf.to_crs(self.config.data_crs)
|
245
|
+
|
246
|
+
successful_downloads = 0
|
247
|
+
with tqdm(total=total_images) as pbar:
|
248
|
+
for row in gdf.itertuples():
|
249
|
+
if download_image(row.Index, tuple(row.geometry.bounds), image_size):
|
250
|
+
successful_downloads += 1
|
251
|
+
pbar.update(1)
|
252
|
+
|
253
|
+
self.logger.info(
|
254
|
+
f"Successfully downloaded {successful_downloads}/{total_images} images!"
|
255
|
+
)
|
256
|
+
|
257
|
+
def download_images_by_coordinates(
|
258
|
+
self,
|
259
|
+
data: Union[pd.DataFrame, List[Tuple[float, float]]],
|
260
|
+
res_meters_pixel: float,
|
261
|
+
output_dir: Union[str, Path],
|
262
|
+
image_size: Tuple[int, int] = (512, 512),
|
263
|
+
image_prefix: str = "maxar_image_",
|
264
|
+
) -> None:
|
265
|
+
"""
|
266
|
+
Download images for given coordinates by creating bounded boxes around points
|
267
|
+
|
268
|
+
Args:
|
269
|
+
data: Either a DataFrame with either latitude/longitude columns or a geometry column or a list of (lat, lon) tuples
|
270
|
+
res_meters_pixel: resolution in meters per pixel
|
271
|
+
output_dir: Directory to save images
|
272
|
+
image_size: Tuple of (width, height) for output images
|
273
|
+
image_prefix: Prefix for output image names
|
274
|
+
"""
|
275
|
+
|
276
|
+
if isinstance(data, pd.DataFrame):
|
277
|
+
coordinates_df = data
|
278
|
+
else:
|
279
|
+
coordinates_df = pd.DataFrame(data, columns=["latitude", "longitude"])
|
280
|
+
|
281
|
+
gdf = convert_to_geodataframe(coordinates_df)
|
282
|
+
|
283
|
+
buffered_gdf = buffer_geodataframe(
|
284
|
+
gdf, res_meters_pixel / 2, cap_style="square"
|
285
|
+
)
|
286
|
+
|
287
|
+
buffered_gdf = buffered_gdf.to_crs(self.config.data_crs)
|
288
|
+
|
289
|
+
self.download_images_by_bounds(
|
290
|
+
buffered_gdf, output_dir, image_size, image_prefix
|
291
|
+
)
|