Skip to content

Commit 4c71520

Browse files
authored
Prep for v0.3.0 (#14)
* Prep for v0.3.0 * Fixed typo * Update changelog
1 parent f409834 commit 4c71520

File tree

6 files changed

+295
-68
lines changed

6 files changed

+295
-68
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ private/
66
**/*.tiff
77
**/*.tif
88
**/*.gpkg
9+
**/*.xml
10+
**/*.shp
11+
**/*.shx
12+
**/*.dbf
913

1014
# C extensions
1115
*.so

docs/changelog.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## v0.3.0 - Apr 26, 2023
4+
5+
**New Features**
6+
7+
- Added several new functions, including `get_basemaps`, `reproject`, `tiff_to_shp`, and `tiff_to_geojson`
8+
- Added hundereds of new basemaps through xyzservices
9+
10+
**Improvement**
11+
12+
- Fixed `tiff_to_vector` crs bug #12
13+
- Add `crs` parameter to `tms_to_geotiff`
14+
315
## v0.2.0 - Apr 21, 2023
416

517
**New Features**

docs/examples/satellite.ipynb

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"import os\n",
6363
"import leafmap\n",
6464
"import torch\n",
65-
"from samgeo import SamGeo, tms_to_geotiff"
65+
"from samgeo import SamGeo, tms_to_geotiff, get_basemaps"
6666
]
6767
},
6868
{
@@ -124,6 +124,31 @@
124124
"# image = '/path/to/your/own/image.tif'"
125125
]
126126
},
127+
{
128+
"attachments": {},
129+
"cell_type": "markdown",
130+
"metadata": {},
131+
"source": [
132+
"Besides the `satellite` basemap, you can use any of the following basemaps returned by the `get_basemaps()` function:"
133+
]
134+
},
135+
{
136+
"cell_type": "code",
137+
"execution_count": null,
138+
"metadata": {},
139+
"outputs": [],
140+
"source": [
141+
"# get_basemaps().keys()"
142+
]
143+
},
144+
{
145+
"attachments": {},
146+
"cell_type": "markdown",
147+
"metadata": {},
148+
"source": [
149+
"Specify the basemap as the source."
150+
]
151+
},
127152
{
128153
"cell_type": "code",
129154
"execution_count": null,
@@ -139,7 +164,7 @@
139164
"metadata": {},
140165
"outputs": [],
141166
"source": [
142-
"m.add_raster(image, layer_name='Image')\n",
167+
"sm.add_raster(image, layer_name='Image')\n",
143168
"m"
144169
]
145170
},

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ onnx
77
geopandas
88
rasterio
99
tqdm
10-
gdown
10+
gdown
11+
xyzservices

samgeo/common.py

Lines changed: 221 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,70 @@ def image_to_cog(source, dst_path=None, profile="deflate", **kwargs):
236236
cog_translate(source, dst_path, dst_profile, **kwargs)
237237

238238

239+
def reproject(
240+
image, output, dst_crs="EPSG:4326", resampling="nearest", to_cog=True, **kwargs
241+
):
242+
"""Reprojects an image.
243+
244+
Args:
245+
image (str): The input image filepath.
246+
output (str): The output image filepath.
247+
dst_crs (str, optional): The destination CRS. Defaults to "EPSG:4326".
248+
resampling (Resampling, optional): The resampling method. Defaults to "nearest".
249+
to_cog (bool, optional): Whether to convert the output image to a Cloud Optimized GeoTIFF. Defaults to True.
250+
**kwargs: Additional keyword arguments to pass to rasterio.open.
251+
252+
"""
253+
import rasterio as rio
254+
from rasterio.warp import calculate_default_transform, reproject, Resampling
255+
256+
if isinstance(resampling, str):
257+
resampling = getattr(Resampling, resampling)
258+
259+
image = os.path.abspath(image)
260+
output = os.path.abspath(output)
261+
262+
if not os.path.exists(os.path.dirname(output)):
263+
os.makedirs(os.path.dirname(output))
264+
265+
with rio.open(image, **kwargs) as src:
266+
transform, width, height = calculate_default_transform(
267+
src.crs, dst_crs, src.width, src.height, *src.bounds
268+
)
269+
kwargs = src.meta.copy()
270+
kwargs.update(
271+
{
272+
"crs": dst_crs,
273+
"transform": transform,
274+
"width": width,
275+
"height": height,
276+
}
277+
)
278+
279+
with rio.open(output, "w", **kwargs) as dst:
280+
for i in range(1, src.count + 1):
281+
reproject(
282+
source=rio.band(src, i),
283+
destination=rio.band(dst, i),
284+
src_transform=src.transform,
285+
src_crs=src.crs,
286+
dst_transform=transform,
287+
dst_crs=dst_crs,
288+
resampling=resampling,
289+
**kwargs,
290+
)
291+
292+
if to_cog:
293+
image_to_cog(output, output)
294+
295+
239296
def tms_to_geotiff(
240297
output,
241298
bbox,
242299
zoom=None,
243300
resolution=None,
244301
source="OpenStreetMap",
302+
crs="EPSG:3857",
245303
to_cog=False,
246304
return_image=False,
247305
overwrite=False,
@@ -258,6 +316,7 @@ def tms_to_geotiff(
258316
resolution (float, optional): The resolution in meters. Defaults to None.
259317
source (str, optional): The tile source. It can be one of the following: "OPENSTREETMAP", "ROADMAP",
260318
"SATELLITE", "TERRAIN", "HYBRID", or an HTTP URL. Defaults to "OpenStreetMap".
319+
crs (str, optional): The output CRS. Defaults to "EPSG:3857".
261320
to_cog (bool, optional): Convert to Cloud Optimized GeoTIFF. Defaults to False.
262321
return_image (bool, optional): Return the image as PIL.Image. Defaults to False.
263322
overwrite (bool, optional): Overwrite the output file if it already exists. Defaults to False.
@@ -296,37 +355,22 @@ def tms_to_geotiff(
296355
return
297356

298357
xyz_tiles = {
299-
"OPENSTREETMAP": {
300-
"url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
301-
"attribution": "OpenStreetMap",
302-
"name": "OpenStreetMap",
303-
},
304-
"ROADMAP": {
305-
"url": "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
306-
"attribution": "Google",
307-
"name": "Google Maps",
308-
},
309-
"SATELLITE": {
310-
"url": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
311-
"attribution": "Google",
312-
"name": "Google Satellite",
313-
},
314-
"TERRAIN": {
315-
"url": "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}",
316-
"attribution": "Google",
317-
"name": "Google Terrain",
318-
},
319-
"HYBRID": {
320-
"url": "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}",
321-
"attribution": "Google",
322-
"name": "Google Satellite",
323-
},
358+
"OPENSTREETMAP": "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
359+
"ROADMAP": "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
360+
"SATELLITE": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
361+
"TERRAIN": "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}",
362+
"HYBRID": "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}",
324363
}
325364

326-
if isinstance(source, str) and source.upper() in xyz_tiles:
327-
source = xyz_tiles[source.upper()]["url"]
328-
elif isinstance(source, str) and source.startswith("http"):
329-
pass
365+
basemaps = get_basemaps()
366+
367+
if isinstance(source, str):
368+
if source.upper() in xyz_tiles:
369+
source = xyz_tiles[source.upper()]
370+
elif source in basemaps:
371+
source = basemaps[source]
372+
elif source.startswith("http"):
373+
pass
330374
else:
331375
raise ValueError(
332376
'source must be one of "OpenStreetMap", "ROADMAP", "SATELLITE", "TERRAIN", "HYBRID", or a URL'
@@ -528,7 +572,9 @@ def draw_tile(
528572
)
529573
if return_image:
530574
return image
531-
if to_cog:
575+
if crs.upper() != "EPSG:3857":
576+
reproject(output, output, crs, to_cog=to_cog)
577+
elif to_cog:
532578
image_to_cog(output, output)
533579
except Exception as e:
534580
raise Exception(e)
@@ -709,3 +755,148 @@ def draw_tile(source, lat0, lon0, lat1, lon1, zoom, filename, **kwargs):
709755
**kwargs,
710756
)
711757
return image
758+
759+
760+
def tiff_to_vector(tiff_path, output, simplify_tolerance=None, **kwargs):
761+
"""Convert a tiff file to a gpkg file.
762+
763+
Args:
764+
tiff_path (str): The path to the tiff file.
765+
output (str): The path to the vector file.
766+
simplify_tolerance (float, optional): The maximum allowed geometry displacement.
767+
The higher this value, the smaller the number of vertices in the resulting geometry.
768+
"""
769+
770+
with rasterio.open(tiff_path) as src:
771+
band = src.read()
772+
773+
mask = band != 0
774+
shapes = features.shapes(band, mask=mask, transform=src.transform)
775+
776+
fc = [
777+
{"geometry": shapely.geometry.shape(shape), "properties": {"value": value}}
778+
for shape, value in shapes
779+
]
780+
if simplify_tolerance is not None:
781+
for i in fc:
782+
i["geometry"] = i["geometry"].simplify(tolerance=simplify_tolerance)
783+
784+
gdf = gpd.GeoDataFrame.from_features(fc)
785+
if src.crs is not None:
786+
gdf.set_crs(crs=src.crs, inplace=True)
787+
gdf.to_file(output, **kwargs)
788+
789+
790+
def tiff_to_gpkg(tiff_path, output, simplify_tolerance=None, **kwargs):
791+
"""Convert a tiff file to a gpkg file.
792+
793+
Args:
794+
tiff_path (str): The path to the tiff file.
795+
output (str): The path to the gpkg file.
796+
simplify_tolerance (float, optional): The maximum allowed geometry displacement.
797+
The higher this value, the smaller the number of vertices in the resulting geometry.
798+
"""
799+
800+
if not output.endswith(".gpkg"):
801+
output += ".gpkg"
802+
803+
tiff_to_vector(tiff_path, output, simplify_tolerance=simplify_tolerance, **kwargs)
804+
805+
806+
def tiff_to_shp(tiff_path, output, simplify_tolerance=None, **kwargs):
807+
"""Convert a tiff file to a shapefile.
808+
809+
Args:
810+
tiff_path (str): The path to the tiff file.
811+
output (str): The path to the shapefile.
812+
simplify_tolerance (float, optional): The maximum allowed geometry displacement.
813+
The higher this value, the smaller the number of vertices in the resulting geometry.
814+
"""
815+
816+
if not output.endswith(".shp"):
817+
output += ".shp"
818+
819+
tiff_to_vector(tiff_path, output, simplify_tolerance=simplify_tolerance, **kwargs)
820+
821+
822+
def tiff_to_geojson(tiff_path, output, simplify_tolerance=None, **kwargs):
823+
"""Convert a tiff file to a GeoJSON file.
824+
825+
Args:
826+
tiff_path (str): The path to the tiff file.
827+
output (str): The path to the GeoJSON file.
828+
simplify_tolerance (float, optional): The maximum allowed geometry displacement.
829+
The higher this value, the smaller the number of vertices in the resulting geometry.
830+
"""
831+
832+
if not output.endswith(".geojson"):
833+
output += ".geojson"
834+
835+
tiff_to_vector(tiff_path, output, simplify_tolerance=simplify_tolerance, **kwargs)
836+
837+
838+
def get_xyz_dict(free_only=True):
839+
"""Returns a dictionary of xyz services.
840+
841+
Args:
842+
free_only (bool, optional): Whether to return only free xyz tile services that do not require an access token. Defaults to True.
843+
844+
Returns:
845+
dict: A dictionary of xyz services.
846+
"""
847+
import collections
848+
import xyzservices.providers as xyz
849+
850+
def _unpack_sub_parameters(var, param):
851+
temp = var
852+
for sub_param in param.split("."):
853+
temp = getattr(temp, sub_param)
854+
return temp
855+
856+
xyz_dict = {}
857+
for item in xyz.values():
858+
try:
859+
name = item["name"]
860+
tile = _unpack_sub_parameters(xyz, name)
861+
if _unpack_sub_parameters(xyz, name).requires_token():
862+
if free_only:
863+
pass
864+
else:
865+
xyz_dict[name] = tile
866+
else:
867+
xyz_dict[name] = tile
868+
869+
except Exception:
870+
for sub_item in item:
871+
name = item[sub_item]["name"]
872+
tile = _unpack_sub_parameters(xyz, name)
873+
if _unpack_sub_parameters(xyz, name).requires_token():
874+
if free_only:
875+
pass
876+
else:
877+
xyz_dict[name] = tile
878+
else:
879+
xyz_dict[name] = tile
880+
881+
xyz_dict = collections.OrderedDict(sorted(xyz_dict.items()))
882+
return xyz_dict
883+
884+
885+
def get_basemaps(free_only=True):
886+
"""Returns a dictionary of xyz basemaps.
887+
888+
Args:
889+
free_only (bool, optional): Whether to return only free xyz tile services that do not require an access token. Defaults to True.
890+
891+
Returns:
892+
dict: A dictionary of xyz basemaps.
893+
"""
894+
895+
basemaps = {}
896+
xyz_dict = get_xyz_dict(free_only=free_only)
897+
for item in xyz_dict:
898+
name = xyz_dict[item].name
899+
url = xyz_dict[item].build_url()
900+
basemaps[name] = url
901+
902+
return basemaps

0 commit comments

Comments
 (0)