Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e9ffed4
Update collections.py
Rkleisley Oct 11, 2024
68d9b39
Update collections.py
Rkleisley Oct 11, 2024
4120510
Update collections.py
Rkleisley Oct 11, 2024
1c338b7
Update collections.py
Rkleisley Oct 11, 2024
af83601
_get_datetime_pandas
Rkleisley Oct 18, 2024
82058b8
parse_arcgis_featureclass
Rkleisley Oct 18, 2024
b062d08
parse_arcpy_featureclass
Rkleisley Oct 18, 2024
c1b1fae
class method for from_arcgis_featureclass
Rkleisley Oct 18, 2024
42903a0
class method for from_arcpy_featureclass
Rkleisley Oct 18, 2024
bca3b69
Update collections.py
Rkleisley Oct 18, 2024
b033fc3
Update parsers.py
Rkleisley Oct 18, 2024
91570d6
Update collections.py
Rkleisley Oct 18, 2024
dae9491
Update parsers.py
Rkleisley Oct 18, 2024
28ca429
Update parsers.py
Rkleisley Oct 29, 2024
90b8c5d
Update parsers.py
Rkleisley Oct 29, 2024
f2fae54
Update structures.py
Rkleisley Oct 29, 2024
f120060
Update collections.py
Rkleisley Oct 29, 2024
fa27e38
Update parsers.py
Rkleisley Oct 29, 2024
00a1c16
Update parsers.py
Rkleisley Oct 29, 2024
59ec916
Update collections.py
Rkleisley Oct 29, 2024
e5a2fce
Update parsers.py
Rkleisley Oct 29, 2024
c3b55b1
Update collections.py
Rkleisley Oct 29, 2024
79adb37
Update structures.py
Rkleisley Oct 29, 2024
c54e401
Update structures.py
Rkleisley Oct 29, 2024
d16fc8b
Update collections.py
Rkleisley Oct 29, 2024
d7814fe
Update collections.py
Rkleisley Oct 29, 2024
c90fdbc
Update structures.py
Rkleisley Oct 29, 2024
5de49ac
Update structures.py
Rkleisley Oct 29, 2024
f525470
Update structures.py
Rkleisley Oct 29, 2024
e96e3eb
Update structures.py
Rkleisley Oct 29, 2024
87a697c
Update structures.py
Rkleisley Oct 29, 2024
944656f
Update structures.py
Rkleisley Oct 29, 2024
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
185 changes: 185 additions & 0 deletions geostructures/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,184 @@ def from_fastkml_folder(cls, folder):

return FeatureCollection(parse_fastkml(folder))

@classmethod
def from_arcgis_featureclass(
cls,
feature_class_path: str,
time_start_property: Optional[str] = None,
time_end_property: Optional[str] = None,
time_fmt: Optional[Union[str, List[str]]] = None,
):
"""
Creates an instance of the class from an ArcGIS feature class.

Args:
feature_class_path (str):
The path to the feature class.
time_start_property (Optional[str]):
The name of the field containing the start time data. Defaults to None.
time_end_property (Optional[str]):
The name of the field containing the end time data. Defaults to None.
time_fmt (Optional[str] or [List[str]]):
The string format(s) of the time properties if they are strs. Defaults to None.

Returns:
An instance of the class populated with geoshapes parsed from the feature class.
"""
from arcgis.features import GeoAcessor # noqa: F401
import pandas as pd
from geostructures.parsers import parse_arcgis_featureclass

_shapes = []
# Convert the feature class into a Spatially Enabled DataFrame (SEDF) for further processing
sedf = pd.DataFrame.spatial.from_featureclass(feature_class_path)
property_columns = [col for col in sedf.columns if col != 'SHAPE']

# Get time values to determine the fmt needed if they are not strings and fmt was not provided
time_start_value, time_end_value = None, None
if time_start_property is not None:
time_start_value = getattr(sedf.iloc[0], time_start_property, None)

if time_end_property is not None:
time_end_value = getattr(sedf.iloc[0], time_start_property, None)

# Pull time format for time_start_property, if it is a string.
if time_start_value and isinstance(time_start_value, str):
if time_fmt is None:
time_fmt = [TimeInterval._get_timeformat(time_start_value)]

if time_end_value and not isinstance(time_end_value, str):
raise TypeError(
f'Time formats cannot be mixed, '
f'Start Time: {type(time_start_value)}, '
f'End Time: {type(time_end_value)}'
)

# Pull time format for time_end_property, if it is a string.
if time_end_value and isinstance(time_end_value, str):
end_time_fmt = [TimeInterval._get_timeformat(time_end_value)]
if time_fmt != end_time_fmt:
if end_time_fmt not in time_fmt:
time_fmt.append(fmt for fmt in end_time_fmt)

if time_start_value and not isinstance(time_start_value, str):
raise TypeError(
f'Time formats cannot be mixed, '
f'Start Time: {type(time_start_value)}, '
f'End Time: {type(time_end_value)}'
)

# Parse each row in SEDF to extract geoshapes and convert them into GeoShapes
for row in sedf.itertuples():
_shapes.append(
parse_arcgis_featureclass(
row,
property_columns,
time_start_property,
time_end_property,
time_fmt
)
)

return cls(_shapes)

@classmethod
def from_arcpy_featureclass(
cls,
feature_class_path: str,
time_start_property: Optional[str] = None,
time_end_property: Optional[str] = None,
time_fmt: Optional[Union[str, List[str]]] = None,
):
"""
Creates an instance of the class from an ArcGIS feature class.

Args:
feature_class_path (str):
The path to the feature class.
time_start_property (Optional[str]):
The name of the field containing the start time data. Defaults to None.
time_end_property (Optional[str]):
The name of the field containing the end time data. Defaults to None.
time_fmt (Optional[str] or [List[str]]):
The string format(s) of the time properties if they are strs. Defaults to None.

Returns:
An instance of the class populated with geoshapes parsed from the feature class.
"""
import arcpy # noqa: F401
from geostructures.parsers import parse_arcpy_featureclass

_shapes = []
# Convert the feature class into a Spatially Enabled DataFrame (SEDF) for further processing
fields = ['SHAPE@']
fields.extend([f.name for f in arcpy.ListFields(feature_class_path) if f.name not in fields])
cursor = arcpy.da.SearchCursor(feature_class_path, fields)
if time_start_property and time_start_property not in fields:
raise ValueError(f'Invalid start time provided: {time_start_property}.')

if time_end_property and time_end_property not in fields:
raise ValueError(f'Invalid end time provided: {time_end_property}.')

# Get time values to determine the fmt needed if they are not strings and fmt was not provided
for row in cursor:
time_start_index, time_end_index = None, None
if time_start_property is not None:
time_start_index = fields.index(time_start_property)

if time_end_property is not None:
time_end_index = fields.index(time_end_property)

time_start_value, time_end_value = None, None
if time_start_index is not None:
time_start_value = row[time_start_index]

if time_end_index is not None:
time_end_value = row[time_end_value]

# Pull time format for time_start_property, if it is a string.
if time_start_value and isinstance(time_start_value, str):
if time_fmt is None:
time_fmt = [TimeInterval._get_timeformat(time_start_value)]

if time_end_value and not isinstance(time_end_value, str):
raise TypeError(
f'Time formats cannot be mixed, '
f'Start Time: {type(time_start_value)}, '
f'End Time: {type(time_end_value)}'
)

# Pull time format for time_end_property, if it is a string.
if time_end_value and isinstance(time_end_value, str):
end_time_fmt = [TimeInterval._get_timeformat(time_end_value)]
if time_fmt != end_time_fmt:
if end_time_fmt not in time_fmt:
time_fmt.append(fmt for fmt in end_time_fmt)

if time_start_value and not isinstance(time_start_value, str):
raise TypeError(
f'Time formats cannot be mixed, '
f'Start Time: {type(time_start_value)}, '
f'End Time: {type(time_end_value)}'
)

break

cursor.reset()
# Parse each row in cursor to extract geoshapes
for row in cursor:
_shapes.append(
parse_arcpy_featureclass(
row,
fields,
time_start_property,
time_end_property,
time_fmt
)
)

return cls(_shapes)

@classmethod
def from_geojson(
cls,
Expand Down Expand Up @@ -405,6 +583,13 @@ def to_fastkml_folder(self, folder_name: str):
features=[x.to_fastkml_placemark() for x in self.geoshapes]
)

def to_featureclass(self, geodatabase, filename):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs a docstring

from arcgis.features import GeoAccessor

gdf = self.to_geopandas()
sedf = GeoAccessor.from_geodataframe(gdf)
sedf.spatial.to_featureclass(f'{geodatabase}\\{filename}')

def to_geojson(self, properties: Optional[Dict] = None, **kwargs):
return {
'type': 'FeatureCollection',
Expand Down
150 changes: 150 additions & 0 deletions geostructures/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@

import json
import re
from datetime import datetime
from typing import cast, Any, Dict, List, Optional, Union

from geostructures.collections import FeatureCollection
from geostructures.structures import GeoPolygon, GeoPoint, GeoLineString
from geostructures.multistructures import MultiGeoPoint, MultiGeoPolygon, MultiGeoLineString
from geostructures.time import TimeInterval
from geostructures.typing import GeoShape, SimpleShape


_PARSER_MAP: Dict[str, SimpleShape] = {
'POINT': GeoPoint,
'POINTGEOMETRY': GeoPoint,
'LINESTRING': GeoLineString,
'POLYGON': GeoPolygon,
'MULTIPOINT': MultiGeoPoint,
Expand All @@ -24,6 +27,153 @@
}


def _get_datetime_pandas(start_time, end_time):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_PARSER_MAP not updated with new types that can show up

"""
Converts pandas Timestamps to Python datetime objects and returns a TimeInterval.

Args:
start_time (pd.Timestamp or None): The start time.
end_time (pd.Timestamp or None): The end time.

Returns:
TimeInterval: The time interval representing the start and end time.
"""
import pandas as pd
from geostructures.time import TimeInterval

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move TimeInterval to top imports


if pd.notnull(start_time) or pd.notnull(end_time):

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's take a quick re-look at this block. If start_time/end_time is not null but is also not a timestamp it could cause issues

if isinstance(start_time, pd.Timestamp):
start_time = start_time.to_pydatetime()

if isinstance(end_time, pd.Timestamp):
end_time = end_time.to_pydatetime()

return TimeInterval(start_time, end_time)


def parse_arcgis_featureclass(
row,
columns,
time_start_property: Optional[Union[str, 'datetime']] = None,
time_end_property: Optional[Union[str, 'datetime']] = None,
time_fmt: Optional[Union[str, List[str]]] = None,
) -> GeoShape:
"""
Parses an ArcGIS feature class row into a geospatial structure.

Args:
row:
The row from an ArcGIS feature class, typically obtained using an arcgis GeoAccessor.

columns:
List of column names to extract attribute values from the row.

time_start_property:
Optional; the column name or datetime representing the start time.

time_end_property:
Optional; the column name or datetime representing the end time.

time_fmt:
Optional; the format or list of formats for parsing time strings.

Returns:
A geospatial object parsed from the feature class row, with attributes and time interval.

Raises:
ValueError: If the geometry type is not supported.
"""
geometry = row.SHAPE
geometry_type_str = type(geometry).__name__.upper()

if geometry_type_str not in _PARSER_MAP:
raise ValueError(f'Unsupported geometry type: {geometry_type_str}.')

parser = _PARSER_MAP[geometry_type_str]
properties = {col: getattr(row, col) for col in columns}
time_start_value, time_end_value = None, None
if time_start_property is not None:
time_start_value = getattr(row, time_start_property, None)

if time_end_property is not None:
time_end_value = getattr(row, time_end_property, None)

dt = None
if time_start_value and isinstance(time_start_value, str):
dt = TimeInterval.from_str(time_start_value, time_end_value, time_fmt)

elif time_start_value:
dt = TimeInterval(time_start_value, time_end_value)

return parser.from_featureclass(
geometry,
dt=dt,
properties=properties
)


def parse_arcpy_featureclass(
row,
fields,
time_start_property: Optional[Union[str, 'datetime']] = None,
time_end_property: Optional[Union[str, 'datetime']] = None,
time_fmt: Optional[Union[str, List[str]]] = None,
) -> GeoShape:
"""
Parses an ArcGIS feature class row into a geospatial structure.

Args:
row: The row from an ArcGIS feature class, typically obtained using an arcpy cursor.
fields: List of column names to extract attribute values from the row.
time_start_property: Optional; the column name or datetime representing the start time.
time_end_property: Optional; the column name or datetime representing the end time.
time_fmt: Optional; the format or list of formats for parsing time strings.

Returns:
A geospatial object parsed from the feature class row, with attributes and time interval.

Raises:
ValueError: If the geometry type is not supported.
"""
geometry_index = fields.index('SHAPE@')
time_start_index, time_end_index = None, None

if time_start_property in fields:
time_start_index = fields.index(time_start_property)

if time_end_property in fields:
time_end_index = fields.index(time_end_property)

geometry = row[geometry_index]
geometry_type_str = type(geometry).__name__.upper()

if geometry_type_str not in _PARSER_MAP:
raise ValueError(f'Unsupported geometry type: {geometry_type_str}.')

parser = _PARSER_MAP[geometry_type_str]
properties = dict(zip(fields, row))
del properties['SHAPE@']
time_start_value, time_end_value = None, None
if time_start_value is not None:
time_start_value = row[time_start_index]

if time_end_value is not None:
time_end_value = row[time_end_index]

dt = None
if time_start_value and isinstance(time_start_value, str):
dt = TimeInterval.from_str(time_start_value, time_end_value, time_fmt)

elif time_start_value:
dt = TimeInterval(time_start_value, time_end_value)

return parser.from_featureclass(
geometry,
dt=dt,
properties=properties
)


def parse_fastkml(
kml,
_shapes: Optional[List[GeoShape]] = None,
Expand Down
Loading