diff --git a/city_metrix/layers/layer.py b/city_metrix/layers/layer.py
index 732992f..299b0a1 100644
--- a/city_metrix/layers/layer.py
+++ b/city_metrix/layers/layer.py
@@ -1,7 +1,11 @@
+import os
from abc import abstractmethod
-from typing import Union, Tuple, List
+from typing import Union, Tuple
+from uuid import uuid4
+from osgeo import gdal
import ee
+import boto3
from dask.diagnostics import ProgressBar
from ee import ImageCollection
from geocube.api.core import make_geocube
@@ -52,6 +56,35 @@ def groupby(self, zones, layer=None):
"""
return LayerGroupBy(self.aggregate, zones, layer, self.masks)
+ def write(self, bbox, output_path, tile_degrees=None):
+ """
+ Write the layer to a path. Does not apply masks.
+
+ :param bbox: (min x, min y, max x, max y)
+ :param output_path: local or s3 path to output to
+ :param tile_degrees: optional param to tile the results into multiple files with a VRT.
+ Degrees to tile by. `output_path` should be a folder path to store the tiles.
+ :return:
+ """
+
+ if tile_degrees is not None:
+ tiles = create_fishnet_grid(*bbox, tile_degrees)
+
+ if not os.path.exists(output_path):
+ os.makedirs(output_path)
+
+ file_names = []
+ for tile in tiles["geometry"]:
+ data = self.aggregate.get_data(tile.bounds)
+
+ file_name = f"{output_path}/{uuid4()}.tif"
+ file_names.append(file_name)
+
+ write_layer(file_name, data)
+ else:
+ data = self.aggregate.get_data(bbox)
+ write_layer(output_path, data)
+
class LayerGroupBy:
def __init__(self, aggregate, zones, layer=None, masks=[]):
@@ -268,3 +301,21 @@ def get_image_collection(
return data
+
+def write_layer(path, data):
+ if isinstance(data, xr.DataArray):
+ # for rasters, need to write to locally first then copy to cloud storage
+ if path.startswith("s3://"):
+ tmp_path = f"{uuid4()}.tif"
+ data.rio.to_raster(raster_path=tmp_path, driver="COG")
+
+ s3 = boto3.client('s3')
+ s3.upload_file(tmp_path, path.split('/')[2], '/'.join(path.split('/')[3:]))
+
+ os.remove(tmp_path)
+ else:
+ data.rio.to_raster(raster_path=path, driver="COG")
+ elif isinstance(data, gpd.GeoDataFrame):
+ data.to_file(path, driver="GeoJSON")
+ else:
+ raise NotImplementedError("Can only write DataArray or GeoDataFrame")
diff --git a/environment.yml b/environment.yml
index 5848dbc..436dbbc 100644
--- a/environment.yml
+++ b/environment.yml
@@ -17,6 +17,7 @@ dependencies:
- dask[complete]=2023.11.0
- matplotlib=3.8.2
- jupyterlab=4.0.10
+ - s3fs=2024.5.0
- geemap=0.32.0
- pip=23.3.1
- pip:
diff --git a/notebooks/tutorial/get layers.ipynb b/notebooks/tutorial/get layers.ipynb
index 883d107..e67ce8a 100644
--- a/notebooks/tutorial/get layers.ipynb
+++ b/notebooks/tutorial/get layers.ipynb
@@ -86,7 +86,24 @@
},
{
"cell_type": "markdown",
- "id": "6280fc2f",
+ "id": "5c506fca",
+ "metadata": {},
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "286103d4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import boto3"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "d4f01f2f-5164-4a05-9997-f70b2abe6b37",
"metadata": {},
"source": [
"In most cases you shouldn't need to set these. GEE will open your browser to authenticate. "
@@ -240,7 +257,7 @@
},
{
"cell_type": "code",
- "execution_count": 7,
+ "execution_count": 5,
"id": "5eb883fd-c533-47b7-8735-65393afca89d",
"metadata": {},
"outputs": [],
@@ -250,7 +267,7 @@
},
{
"cell_type": "code",
- "execution_count": 8,
+ "execution_count": 6,
"id": "0703cfe0-201d-4cae-ab43-7a4d0e5082cf",
"metadata": {},
"outputs": [
@@ -258,8 +275,8 @@
"name": "stdout",
"output_type": "stream",
"text": [
- "Extracting tree cover layer:\n",
- "[########################################] | 100% Completed | 19.58 s\n"
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 9.24 ss\n"
]
},
{
@@ -628,40 +645,40 @@
" stroke: currentColor;\n",
" fill: currentColor;\n",
"}\n",
- "
<xarray.DataArray 'ttc' (y: 2878, x: 3721)>\n",
- "array([[ nan, nan, nan, ..., 100., 100., 90.],\n",
- " [ nan, nan, nan, ..., 90., 90., 90.],\n",
+ "<xarray.DataArray 'ttc' (y: 2878, x: 3722)>\n",
+ "array([[ nan, nan, nan, ..., 100., 90., 90.],\n",
" [ nan, nan, nan, ..., 90., 90., 90.],\n",
+ " [ nan, nan, nan, ..., 90., 90., 80.],\n",
" ...,\n",
" [ nan, nan, nan, ..., nan, nan, nan],\n",
" [ nan, nan, nan, ..., nan, nan, nan],\n",
" [ nan, nan, nan, ..., nan, nan, nan]])\n",
"Coordinates:\n",
- " time int32 0\n",
+ " time int64 0\n",
" * x (x) float32 -38.65 -38.65 -38.65 -38.65 ... -38.3 -38.3 -38.3 -38.3\n",
" * y (y) float32 -12.76 -12.76 -12.76 -12.76 ... -13.02 -13.02 -13.02\n",
"Attributes:\n",
" id: ttc\n",
" data_type: {'type': 'PixelType', 'precision': 'double', 'min': 0, 'm...\n",
" crs: EPSG:4326\n",
- " crs_transform: [1, 0, 0, 0, 1, 0]
nan nan nan nan nan nan nan nan ... nan nan nan nan nan nan nan nan
array([[ nan, nan, nan, ..., 100., 100., 90.],\n",
- " [ nan, nan, nan, ..., 90., 90., 90.],\n",
+ " crs_transform: [1, 0, 0, 0, 1, 0]
nan nan nan nan nan nan nan nan ... nan nan nan nan nan nan nan nan
array([[ nan, nan, nan, ..., 100., 90., 90.],\n",
" [ nan, nan, nan, ..., 90., 90., 90.],\n",
+ " [ nan, nan, nan, ..., 90., 90., 80.],\n",
" ...,\n",
" [ nan, nan, nan, ..., nan, nan, nan],\n",
" [ nan, nan, nan, ..., nan, nan, nan],\n",
- " [ nan, nan, nan, ..., nan, nan, nan]])
PandasIndex
PandasIndex(Index([ -38.64730453491211, -38.647212982177734, -38.64712142944336,\n",
+ " [ nan, nan, nan, ..., nan, nan, nan]])
PandasIndex
PandasIndex(Index([ -38.64730453491211, -38.647212982177734, -38.64712142944336,\n",
" -38.647029876708984, -38.64693832397461, -38.646846771240234,\n",
" -38.64675521850586, -38.64665985107422, -38.646568298339844,\n",
" -38.64647674560547,\n",
" ...\n",
- " -38.30543899536133, -38.30534744262695, -38.30525588989258,\n",
- " -38.30516052246094, -38.30506896972656, -38.30497741699219,\n",
- " -38.30488586425781, -38.30479431152344, -38.30470275878906,\n",
- " -38.30460739135742],\n",
- " dtype='float32', name='x', length=3721))
PandasIndex
PandasIndex(Index([-12.757013320922852, -12.75710391998291, -12.757194519042969,\n",
+ " -38.30534744262695, -38.30525588989258, -38.30516052246094,\n",
+ " -38.30506896972656, -38.30497741699219, -38.30488586425781,\n",
+ " -38.30479431152344, -38.30470275878906, -38.30460739135742,\n",
+ " -38.30451583862305],\n",
+ " dtype='float32', name='x', length=3722))
PandasIndex
PandasIndex(Index([-12.757013320922852, -12.75710391998291, -12.757194519042969,\n",
" -12.757284164428711, -12.75737476348877, -12.757465362548828,\n",
" -12.757555961608887, -12.757646560668945, -12.757737159729004,\n",
" -12.757826805114746,\n",
@@ -670,19 +687,19 @@
" -13.016627311706543, -13.016716957092285, -13.016807556152344,\n",
" -13.016898155212402, -13.016988754272461, -13.01707935333252,\n",
" -13.017169952392578],\n",
- " dtype='float32', name='y', length=2878))
- id :
- ttc
- data_type :
- {'type': 'PixelType', 'precision': 'double', 'min': 0, 'max': 255}
- crs :
- EPSG:4326
- crs_transform :
- [1, 0, 0, 0, 1, 0]
"
+ " dtype='float32', name='y', length=2878))
- id :
- ttc
- data_type :
- {'type': 'PixelType', 'precision': 'double', 'min': 0, 'max': 255}
- crs :
- EPSG:4326
- crs_transform :
- [1, 0, 0, 0, 1, 0]
"
],
"text/plain": [
- "\n",
- "array([[ nan, nan, nan, ..., 100., 100., 90.],\n",
- " [ nan, nan, nan, ..., 90., 90., 90.],\n",
+ "\n",
+ "array([[ nan, nan, nan, ..., 100., 90., 90.],\n",
" [ nan, nan, nan, ..., 90., 90., 90.],\n",
+ " [ nan, nan, nan, ..., 90., 90., 80.],\n",
" ...,\n",
" [ nan, nan, nan, ..., nan, nan, nan],\n",
" [ nan, nan, nan, ..., nan, nan, nan],\n",
" [ nan, nan, nan, ..., nan, nan, nan]])\n",
"Coordinates:\n",
- " time int32 0\n",
+ " time int64 0\n",
" * x (x) float32 -38.65 -38.65 -38.65 -38.65 ... -38.3 -38.3 -38.3 -38.3\n",
" * y (y) float32 -12.76 -12.76 -12.76 -12.76 ... -13.02 -13.02 -13.02\n",
"Attributes:\n",
@@ -692,7 +709,7 @@
" crs_transform: [1, 0, 0, 0, 1, 0]"
]
},
- "execution_count": 8,
+ "execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
@@ -703,6 +720,75 @@
"city_TreeCover"
]
},
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "8c2477dc",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 8.91 ss\n"
+ ]
+ }
+ ],
+ "source": [
+ "city_TreeCover = TreeCover().write(city_gdf.total_bounds, \"output.tif\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "cb58b256",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.63 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.52 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.53 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.38 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.35 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 2.47 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.53 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.45 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.46 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.54 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.69 sms\n",
+ "Extracting layer tree cover from Google Earth Engine:\n",
+ "[########################################] | 100% Completed | 1.59 sms\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "/usr/local/anaconda3/envs/cities-cif/lib/python3.10/site-packages/osgeo/gdal.py:287: FutureWarning: Neither gdal.UseExceptions() nor gdal.DontUseExceptions() has been explicitly called. In GDAL 4.0, exceptions will be enabled by default.\n",
+ " warnings.warn(\n",
+ "ERROR 4: Failed to open output to write.\n"
+ ]
+ }
+ ],
+ "source": [
+ "city_TreeCover = TreeCover().write(city_gdf.total_bounds, \"output\", tile_degrees=0.1)"
+ ]
+ },
{
"cell_type": "code",
"execution_count": 9,
diff --git a/setup.py b/setup.py
index bff48fe..c2d24d8 100644
--- a/setup.py
+++ b/setup.py
@@ -20,5 +20,6 @@
"utm",
"osmnx",
"geopandas",
+ "s3fs",
],
)