Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Make a lazy loading of gee gdf..close #919. #922

Merged
merged 6 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 63 additions & 20 deletions sepal_ui/aoi/aoi_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,6 @@ def _from_asset(self, asset_json: dict) -> Self:
# set the feature collection
self.feature_collection = ee_col

# create a gdf form the feature_collection
features = self.feature_collection.getInfo()["features"]
self.gdf = gpd.GeoDataFrame.from_features(features).set_crs(epsg=4326)

return self

def _from_points(self, point_json: dict) -> Self:
Expand Down Expand Up @@ -377,21 +373,28 @@ def _from_admin(self, admin: str) -> Self:
# pygaul needs extra work as ISO codes are not included in the GEE dataset
if self.gee:
self.feature_collection = pygaul.AdmItems(admin=admin)
features = self.feature_collection.getInfo()["features"]
self.gdf = gpd.GeoDataFrame.from_features(features).set_crs(epsg=4326)
gaul_country = str(self.gdf.ADM0_CODE.unique()[0])
iso = json.loads(self.MAPPING.read_text())[gaul_country]
self.gdf["ISO"] = iso

# get the ADM0_CODE to get the ISO code
feature = self.feature_collection.first()
properties = feature.toDictionary(feature.propertyNames()).getInfo()

iso = json.loads(self.MAPPING.read_text())[str(properties.get("ADM0_CODE"))]
names = [value for prop, value in properties.items() if "NAME" in prop]

# generate the name from the columns
names = [su.normalize_str(name) for name in names]
names[0] = iso

self.name = "_".join(names)

else:
self.gdf = pygadm.AdmItems(admin=admin)

# generate the name from the columns
r = self.gdf.iloc[0]
names = [su.normalize_str(r[c]) for c in self.gdf.columns if "NAME" in c]
names[0] = r.ISO if self.gee else r.GID_0[:3]
self.name = "_".join(names)

# generate the name from the columns
r = self.gdf.iloc[0]
names = [su.normalize_str(r[c]) for c in self.gdf.columns if "NAME" in c]
names[0] = r.GID_0[:3]
self.name = "_".join(names)
return self

def clear_output(self) -> Self:
Expand Down Expand Up @@ -433,7 +436,7 @@ def get_columns(self) -> List[str]:
Returns:
sorted list of column names
"""
if self.gdf is None:
if self._gdf is None and not self.feature_collection:
raise Exception(ms.aoi_sel.exception.no_gdf)

if self.gee:
Expand All @@ -455,7 +458,7 @@ def get_fields(self, column: str) -> List[str]:
sorted list of fields value

"""
if self.gdf is None:
if self._gdf is None and not self.feature_collection:
raise Exception(ms.aoi_sel.exception.no_gdf)

if self.gee:
Expand All @@ -476,7 +479,7 @@ def get_selected(self, column: str, field: str) -> Union[ee.Feature, gpd.GeoData
Returns:
The Feature associated with the query
"""
if self.gdf is None:
if self._gdf is None and not self.feature_collection:
raise Exception(ms.aoi_sel.exception.no_gdf)

if self.gee:
Expand All @@ -492,10 +495,17 @@ def total_bounds(self) -> Tuple[float, float, float, float]:
Returns:
minxx, miny, maxx, maxy
"""
if self.gdf is None:
# use _gdf to evaluate the condition to avoid accessing the gdf property
if self._gdf is None and not self.feature_collection:
raise ValueError(ms.aoi_sel.exception.no_gdf)

return self.gdf.total_bounds.tolist()
if self.gee:
coords = self.feature_collection.geometry().bounds().coordinates().get(0).getInfo()
bounds = [coords[0][0], coords[0][1], coords[3][0], coords[3][1]]
else:
bounds = self.gdf.total_bounds.tolist()

return [round(bound, 4) for bound in bounds]

def export_to_asset(self) -> Self:
"""Export the feature_collection as an asset (only for ee model)."""
Expand Down Expand Up @@ -533,6 +543,8 @@ def get_ipygeojson(self, style: Optional[dict] = None) -> GeoJSON:
Returns:
The geojson layer of the aoi gdf, ready to use in a Map
"""
# This function aims to work in the same way in both gee and non-gee mode
# It's why we use the gdf property to evaluate the condition
if self.gdf is None:
raise Exception(ms.aoi_sel.exception.no_gdf)

Expand All @@ -553,3 +565,34 @@ def get_ipygeojson(self, style: Optional[dict] = None) -> GeoJSON:
self.ipygeojson = GeoJSON(data=data, style=style, name="aoi")

return self.ipygeojson

@property
def gdf(self):
"""Get the geodataframe associated with the AOI."""
if self.gee:
if self._gdf is not None:
# This happens when it comes from vector or geojson
return self._gdf

if not self.feature_collection:
return None

self._load_gdf()

return self._gdf

@gdf.setter
def gdf(self, value):
"""Set the gdf value. Used mainly to reset the gdf value."""
self._gdf = value

def _load_gdf(self):
"""Return a geodataframe from a feature collection."""
features = self.feature_collection.getInfo()["features"]
self._gdf = gpd.GeoDataFrame.from_features(features).set_crs(epsg=4326)

if self.method in ["ADMIN0", "ADMIN1", "ADMIN2"]:

gaul_country = str(self._gdf.ADM0_CODE.unique()[0])
iso = json.loads(self.MAPPING.read_text())[gaul_country]
self._gdf["ISO"] = iso
6 changes: 5 additions & 1 deletion sepal_ui/aoi/aoi_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,11 @@ def _update_aoi(self, *args) -> Self:
if self.map_:
self.map_.remove_layer("aoi", none_ok=True)
self.map_.zoom_bounds(self.model.total_bounds())
self.map_.add_layer(self.model.get_ipygeojson(self.map_style))

if self.gee:
self.map_.add_ee_layer(self.model.feature_collection, {}, "aoi")
else:
self.map_.add_layer(self.model.get_ipygeojson(self.map_style), "aoi")

self.aoi_dc.hide()

Expand Down
3 changes: 2 additions & 1 deletion sepal_ui/message/en/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@
"no_draw": "Please draw a shape in the map",
"no_admlyr": "Select an administrative layer",
"invalid_code": "The code is not in the database",
"no_gdf": "You must set the gdf before interacting with it"
"no_gdf": "You must set the gdf before interacting with it",
"no_fc": "You have to select a feature collection first"
}
},
"mapping": {
Expand Down
49 changes: 49 additions & 0 deletions tests/test_aoi/test_AoiModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,34 @@ def test_from_vector(gee_dir: Path, fake_vector: dict) -> None:
return


@pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set")
def test_from_vector_gee(gee_dir: Path, fake_vector: dict) -> None:
"""Get an AoiModel from a vector and using GEE.

Args:
gee_dir: the path to the session gee_dir folder (including hash)
fake_vector: the path to a vector file
"""
aoi_model = aoi.AoiModel(folder=gee_dir, gee=True)

# with no pathname
with pytest.raises(Exception):
aoi_model._from_vector(fake_vector)

# all params
vector = {"pathname": fake_vector, "column": "GID_0", "value": "VAT"}
aoi_model._from_vector(vector)
assert aoi_model.name == "gadm41_VAT_0_GID_0_VAT"

# Check that the vector was converted to feature_collection
assert aoi_model.feature_collection is not None
assert aoi_model.feature_collection.first().toDictionary().values().getInfo() == [
"VaticanCity",
"VAT",
]
assert aoi_model.gdf is not None


@pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set")
def test_from_geo_json(gee_dir, square: dict) -> None:
"""Get an AoiModel from a geojson (equivalent to draw).
Expand All @@ -354,6 +382,27 @@ def test_from_geo_json(gee_dir, square: dict) -> None:
return


@pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set")
def test_from_geo_json_gee(gee_dir, square: dict) -> None:
"""Get an AoiModel from a geojson (equivalent to draw).

Args:
gee_dir: the path to the session gee_dir folder (including hash)
square: the geo_interface representation of a quare around vatican
"""
aoi_model = aoi.AoiModel(folder=gee_dir, gee=True)

# fully qualified square
aoi_model.name = "square"
aoi_model._from_geo_json(square)
assert aoi_model.name == "square"

# Check the feature collection exists
assert aoi_model.feature_collection.getInfo()

return


@pytest.mark.skipif(not ee.data._credentials, reason="GEE is not set")
def test_from_asset(gee_dir: Path) -> Path:
"""Get an AoiModel from gee assets.
Expand Down
Loading