Skip to content
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
65 changes: 65 additions & 0 deletions docs/guides/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,71 @@ road = features.create_road_feature_from_osm(in_place=True)
assert features.road is road
```

### Create Road Features from GeoJSON/GeoDataFrame

You can also create road features from your own GeoJSON data or GeoPandas GeoDataFrame. This is particularly useful for:
- Custom road data not available in OSM
- Local coordinate system domains (OSM is not supported for local CRS)
- Integration with existing geospatial workflows

#### Using GeoJSON

```python
# Define your road areas as GeoJSON
geojson_data = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[
[-120.5, 39.5],
[-120.5, 39.6],
[-120.4, 39.6],
[-120.4, 39.5],
[-120.5, 39.5]
]]
},
"properties": {}
}]
}

# Create road features from GeoJSON
road = features.create_road_feature(sources="geojson", geojson=geojson_data)
```

#### Using GeoPandas GeoDataFrame

```python
import geopandas as gpd
from shapely.geometry import Polygon

# Create a GeoDataFrame with road polygons
polygon = Polygon([
(-120.5, 39.5),
(-120.5, 39.6),
(-120.4, 39.6),
(-120.4, 39.5),
(-120.5, 39.5)
])

gdf = gpd.GeoDataFrame({
'geometry': [polygon],
'name': ['highway_1']
}, crs="EPSG:4326")

# Create road features from GeoDataFrame
road = features.create_road_feature_from_geodataframe(gdf)

# Or with in_place=True
road = features.create_road_feature_from_geodataframe(gdf, in_place=True)
```

!!! note "GeoJSON Requirements"
- Must contain Polygon or MultiPolygon geometries (area-based features)
- Geometries will be automatically clipped to the domain boundary
- For non-local CRS, geometries are transformed to match the domain's CRS

### Wait for Processing

Road feature creation happens asynchronously. Wait for processing to complete:
Expand Down
3 changes: 3 additions & 0 deletions fastfuels_sdk/client_library/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"GeoJSONStyleProperties",
"GeoJsonCRS",
"GeoJsonCRSProperties",
"Geojson",
"Geometry",
"GridAttributeMetadataResponse",
"Grids",
Expand Down Expand Up @@ -223,6 +224,7 @@
from fastfuels_sdk.client_library.models.geo_json_style_properties import GeoJSONStyleProperties as GeoJSONStyleProperties
from fastfuels_sdk.client_library.models.geo_json_crs import GeoJsonCRS as GeoJsonCRS
from fastfuels_sdk.client_library.models.geo_json_crs_properties import GeoJsonCRSProperties as GeoJsonCRSProperties
from fastfuels_sdk.client_library.models.geojson import Geojson as Geojson
from fastfuels_sdk.client_library.models.geometry import Geometry as Geometry
from fastfuels_sdk.client_library.models.grid_attribute_metadata_response import GridAttributeMetadataResponse as GridAttributeMetadataResponse
from fastfuels_sdk.client_library.models.grids import Grids as Grids
Expand Down Expand Up @@ -315,3 +317,4 @@
from fastfuels_sdk.client_library.models.validation_error_loc_inner import ValidationErrorLocInner as ValidationErrorLocInner
from fastfuels_sdk.client_library.models.water_feature import WaterFeature as WaterFeature
from fastfuels_sdk.client_library.models.water_feature_source import WaterFeatureSource as WaterFeatureSource

6 changes: 3 additions & 3 deletions fastfuels_sdk/client_library/api/road_feature_api.py

Large diffs are not rendered by default.

13 changes: 8 additions & 5 deletions fastfuels_sdk/client_library/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import os
import re
import tempfile
import uuid

from urllib.parse import quote
from typing import Tuple, Optional, List, Dict, Union
Expand Down Expand Up @@ -356,6 +357,8 @@ def sanitize_for_serialization(self, obj):
return obj.get_secret_value()
elif isinstance(obj, self.PRIMITIVE_TYPES):
return obj
elif isinstance(obj, uuid.UUID):
return str(obj)
elif isinstance(obj, list):
return [
self.sanitize_for_serialization(sub_obj) for sub_obj in obj
Expand Down Expand Up @@ -408,7 +411,7 @@ def deserialize(self, response_text: str, response_type: str, content_type: Opti
data = json.loads(response_text)
except ValueError:
data = response_text
elif re.match(r'^application/(json|[\w!#$&.+-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE):
elif re.match(r'^application/(json|[\w!#$&.+\-^_]+\+json)\s*(;|$)', content_type, re.IGNORECASE):
if response_text == "":
data = ""
else:
Expand Down Expand Up @@ -457,13 +460,13 @@ def __deserialize(self, data, klass):

if klass in self.PRIMITIVE_TYPES:
return self.__deserialize_primitive(data, klass)
elif klass == object:
elif klass is object:
return self.__deserialize_object(data)
elif klass == datetime.date:
elif klass is datetime.date:
return self.__deserialize_date(data)
elif klass == datetime.datetime:
elif klass is datetime.datetime:
return self.__deserialize_datetime(data)
elif klass == decimal.Decimal:
elif klass is decimal.Decimal:
return decimal.Decimal(data)
elif issubclass(klass, Enum):
return self.__deserialize_enum(data, klass)
Expand Down
2 changes: 1 addition & 1 deletion fastfuels_sdk/client_library/api_spec.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion fastfuels_sdk/client_library/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
Do not edit the class manually.
""" # noqa: E501


# import models into model package
from fastfuels_sdk.client_library.models.access import Access
from fastfuels_sdk.client_library.models.application import Application
Expand Down Expand Up @@ -43,6 +42,7 @@
from fastfuels_sdk.client_library.models.geo_json_style_properties import GeoJSONStyleProperties
from fastfuels_sdk.client_library.models.geo_json_crs import GeoJsonCRS
from fastfuels_sdk.client_library.models.geo_json_crs_properties import GeoJsonCRSProperties
from fastfuels_sdk.client_library.models.geojson import Geojson
from fastfuels_sdk.client_library.models.geometry import Geometry
from fastfuels_sdk.client_library.models.grid_attribute_metadata_response import GridAttributeMetadataResponse
from fastfuels_sdk.client_library.models.grids import Grids
Expand Down Expand Up @@ -135,3 +135,4 @@
from fastfuels_sdk.client_library.models.validation_error_loc_inner import ValidationErrorLocInner
from fastfuels_sdk.client_library.models.water_feature import WaterFeature
from fastfuels_sdk.client_library.models.water_feature_source import WaterFeatureSource

19 changes: 15 additions & 4 deletions fastfuels_sdk/client_library/models/create_road_feature_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@
import json

from pydantic import BaseModel, ConfigDict, Field
from typing import Any, ClassVar, Dict, List
from typing import Any, ClassVar, Dict, List, Optional
from fastfuels_sdk.client_library.models.geojson import Geojson
from fastfuels_sdk.client_library.models.road_feature_source import RoadFeatureSource
from typing import Optional, Set
from typing_extensions import Self

class CreateRoadFeatureRequest(BaseModel):
"""
CreateRoadFeatureRequest
Request model for creating road features with validation.
""" # noqa: E501
sources: List[RoadFeatureSource] = Field(description="List of sources of road features")
__properties: ClassVar[List[str]] = ["sources"]
geojson: Optional[Geojson] = None
__properties: ClassVar[List[str]] = ["sources", "geojson"]

model_config = ConfigDict(
populate_by_name=True,
Expand Down Expand Up @@ -69,6 +71,14 @@ def to_dict(self) -> Dict[str, Any]:
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of geojson
if self.geojson:
_dict['geojson'] = self.geojson.to_dict()
# set to None if geojson (nullable) is None
# and model_fields_set contains the field
if self.geojson is None and "geojson" in self.model_fields_set:
_dict['geojson'] = None

return _dict

@classmethod
Expand All @@ -81,7 +91,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
return cls.model_validate(obj)

_obj = cls.model_validate({
"sources": obj.get("sources")
"sources": obj.get("sources"),
"geojson": Geojson.from_dict(obj["geojson"]) if obj.get("geojson") is not None else None
})
return _obj

Expand Down
140 changes: 140 additions & 0 deletions fastfuels_sdk/client_library/models/geojson.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# coding: utf-8

"""
FastFuels API

A JSON API for creating, editing, and retrieving 3D fuels data for next generation fire behavior models.

The version of the OpenAPI document: 0.1.0
Generated by OpenAPI Generator (https://openapi-generator.tech)

Do not edit the class manually.
""" # noqa: E501


from __future__ import annotations
from inspect import getfullargspec
import json
import pprint
import re # noqa: F401
from pydantic import BaseModel, ConfigDict, Field, StrictStr, ValidationError, field_validator
from typing import Optional
from fastfuels_sdk.client_library.models.geo_json_feature import GeoJSONFeature
from fastfuels_sdk.client_library.models.geo_json_feature_collection import GeoJSONFeatureCollection
from typing import Union, Any, List, Set, TYPE_CHECKING, Optional, Dict
from typing_extensions import Literal, Self
from pydantic import Field

GEOJSON_ANY_OF_SCHEMAS = ["GeoJSONFeature", "GeoJSONFeatureCollection"]

class Geojson(BaseModel):
"""
GeoJSON input when source is geojson. Must contain Polygon or MultiPolygon geometries.
"""

# data type: GeoJSONFeature
anyof_schema_1_validator: Optional[GeoJSONFeature] = None
# data type: GeoJSONFeatureCollection
anyof_schema_2_validator: Optional[GeoJSONFeatureCollection] = None
if TYPE_CHECKING:
actual_instance: Optional[Union[GeoJSONFeature, GeoJSONFeatureCollection]] = None
else:
actual_instance: Any = None
any_of_schemas: Set[str] = { "GeoJSONFeature", "GeoJSONFeatureCollection" }

model_config = {
"validate_assignment": True,
"protected_namespaces": (),
}

def __init__(self, *args, **kwargs) -> None:
if args:
if len(args) > 1:
raise ValueError("If a position argument is used, only 1 is allowed to set `actual_instance`")
if kwargs:
raise ValueError("If a position argument is used, keyword arguments cannot be used.")
super().__init__(actual_instance=args[0])
else:
super().__init__(**kwargs)

@field_validator('actual_instance')
def actual_instance_must_validate_anyof(cls, v):
if v is None:
return v

instance = Geojson.model_construct()
error_messages = []
# validate data type: GeoJSONFeature
if not isinstance(v, GeoJSONFeature):
error_messages.append(f"Error! Input type `{type(v)}` is not `GeoJSONFeature`")
else:
return v

# validate data type: GeoJSONFeatureCollection
if not isinstance(v, GeoJSONFeatureCollection):
error_messages.append(f"Error! Input type `{type(v)}` is not `GeoJSONFeatureCollection`")
else:
return v

if error_messages:
# no match
raise ValueError("No match found when setting the actual_instance in Geojson with anyOf schemas: GeoJSONFeature, GeoJSONFeatureCollection. Details: " + ", ".join(error_messages))
else:
return v

@classmethod
def from_dict(cls, obj: Dict[str, Any]) -> Self:
return cls.from_json(json.dumps(obj))

@classmethod
def from_json(cls, json_str: str) -> Self:
"""Returns the object represented by the json string"""
instance = cls.model_construct()
if json_str is None:
return instance

error_messages = []
# anyof_schema_1_validator: Optional[GeoJSONFeature] = None
try:
instance.actual_instance = GeoJSONFeature.from_json(json_str)
return instance
except (ValidationError, ValueError) as e:
error_messages.append(str(e))
# anyof_schema_2_validator: Optional[GeoJSONFeatureCollection] = None
try:
instance.actual_instance = GeoJSONFeatureCollection.from_json(json_str)
return instance
except (ValidationError, ValueError) as e:
error_messages.append(str(e))

if error_messages:
# no match
raise ValueError("No match found when deserializing the JSON string into Geojson with anyOf schemas: GeoJSONFeature, GeoJSONFeatureCollection. Details: " + ", ".join(error_messages))
else:
return instance

def to_json(self) -> str:
"""Returns the JSON representation of the actual instance"""
if self.actual_instance is None:
return "null"

if hasattr(self.actual_instance, "to_json") and callable(self.actual_instance.to_json):
return self.actual_instance.to_json()
else:
return json.dumps(self.actual_instance)

def to_dict(self) -> Optional[Union[Dict[str, Any], GeoJSONFeature, GeoJSONFeatureCollection]]:
"""Returns the dict representation of the actual instance"""
if self.actual_instance is None:
return None

if hasattr(self.actual_instance, "to_dict") and callable(self.actual_instance.to_dict):
return self.actual_instance.to_dict()
else:
return self.actual_instance

def to_str(self) -> str:
"""Returns the string representation of the actual instance"""
return pprint.pformat(self.model_dump())


15 changes: 13 additions & 2 deletions fastfuels_sdk/client_library/models/road_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,23 @@
from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from fastfuels_sdk.client_library.models.geojson import Geojson
from fastfuels_sdk.client_library.models.job_status import JobStatus
from fastfuels_sdk.client_library.models.road_feature_source import RoadFeatureSource
from typing import Optional, Set
from typing_extensions import Self

class RoadFeature(BaseModel):
"""
RoadFeature
Response model for road features without strict validation.
""" # noqa: E501
sources: List[RoadFeatureSource] = Field(description="List of sources of road features")
geojson: Optional[Geojson] = None
status: Optional[JobStatus] = None
created_on: Optional[datetime] = Field(default=None, alias="createdOn")
modified_on: Optional[datetime] = Field(default=None, alias="modifiedOn")
checksum: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["sources", "status", "createdOn", "modifiedOn", "checksum"]
__properties: ClassVar[List[str]] = ["sources", "geojson", "status", "createdOn", "modifiedOn", "checksum"]

model_config = ConfigDict(
populate_by_name=True,
Expand Down Expand Up @@ -75,6 +77,14 @@ def to_dict(self) -> Dict[str, Any]:
exclude=excluded_fields,
exclude_none=True,
)
# override the default output from pydantic by calling `to_dict()` of geojson
if self.geojson:
_dict['geojson'] = self.geojson.to_dict()
# set to None if geojson (nullable) is None
# and model_fields_set contains the field
if self.geojson is None and "geojson" in self.model_fields_set:
_dict['geojson'] = None

# set to None if status (nullable) is None
# and model_fields_set contains the field
if self.status is None and "status" in self.model_fields_set:
Expand Down Expand Up @@ -108,6 +118,7 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:

_obj = cls.model_validate({
"sources": obj.get("sources"),
"geojson": Geojson.from_dict(obj["geojson"]) if obj.get("geojson") is not None else None,
"status": obj.get("status"),
"createdOn": obj.get("createdOn"),
"modifiedOn": obj.get("modifiedOn"),
Expand Down
Loading
Loading