Skip to content
Closed
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
17 changes: 12 additions & 5 deletions api/geospatial.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,13 @@ def get_feature_collection(

things = get_thing_features(session, thing_type, group)

def make_feature_dict(thing, geometry, elevation, *other):
geometry = json.loads(geometry)
geometry["coordinates"].append(elevation)
def make_feature_dict(thing):
current_location = thing.current_location
x = current_location.latlon[1]
y = current_location.latlon[0]
elevation = current_location.elevation
Comment on lines +104 to +108

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard against missing current_location in geospatial features

make_feature_dict now dereferences thing.current_location and its latlon/elevation without checking for a valid location. When a thing has no active location association (current_location returns None, e.g., its latest association has effective_end set), this code raises before building the FeatureCollection, causing the /geospatial (and shapefile) endpoints to 500. Previously the query filtered to active locations (effective_end is NULL), so such records were skipped. Please short-circuit or filter out things without a current location before accessing coordinates.

Useful? React with 👍 / 👎.

coordinates = [x, y, elevation]

return {
"type": "Feature",
"properties": {
Expand All @@ -112,10 +116,13 @@ def make_feature_dict(thing, geometry, elevation, *other):
"name": thing.name,
"group": group,
},
"geometry": geometry,
"geometry": {
"type": "Point",
"coordinates": coordinates,
},
}

features = [make_feature_dict(*item) for item in things]
features = [make_feature_dict(thing) for thing in things]

return {
"type": "FeatureCollection",
Expand Down
51 changes: 8 additions & 43 deletions services/geospatial_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,55 +20,18 @@
import constants
from db.thing import Thing
from db.group import GroupThingAssociation, Group
from db.location import Location, LocationThingAssociation
from geoalchemy2.functions import ST_GeomFromText, ST_Within, ST_AsGeoJSON
from db.location import Location
from geoalchemy2.functions import ST_GeomFromText, ST_Within
from geoalchemy2.shape import to_shape
from shapely.wkt import loads as wkt_loads
from sqlalchemy import Select, select
from sqlalchemy.orm import aliased
from sqlalchemy import func
from sqlalchemy import Select


def get_thing_features(
session, thing_type: list | str | None, group: str | int | None
) -> list:
# sql = (
# select(Thing, ST_AsGeoJSON(Location.point).label("geojson"))
# .join(LocationThingAssociation, Thing.id == LocationThingAssociation.thing_id)
# .join(Location, LocationThingAssociation.location_id == Location.id)
# )

# selection_args = [Thing, ST_AsGeoJSON(Location.point).label("geojson")]
# if thing_type == "well":
# selection_args.append(WellThing)
# elif thing_type == "spring":
# selection_args.append(SpringThing)

# Subquery: get the latest association for each thing (optionally only active)
lta_alias = aliased(LocationThingAssociation)

latest_assoc = (
select(
LocationThingAssociation.thing_id,
func.max(LocationThingAssociation.effective_start).label("max_start"),
)
.where(
LocationThingAssociation.effective_end == None
) # Only active, remove if you want most recent regardless of end
.group_by(LocationThingAssociation.thing_id)
.subquery()
)

sql = (
select(Thing, ST_AsGeoJSON(Location.point).label("geojson"), Location.elevation)
.join(lta_alias, Thing.id == lta_alias.thing_id)
.join(Location, lta_alias.location_id == Location.id)
.join(
latest_assoc,
(latest_assoc.c.thing_id == lta_alias.thing_id)
& (latest_assoc.c.max_start == lta_alias.effective_start),
)
)
sql = session.query(Thing)

if thing_type:
if isinstance(thing_type, str):
Expand All @@ -88,7 +51,7 @@ def get_thing_features(
sql = sql.where(Group.id == group)

# unique needs to be invoked to prevent duplicates from eager loading
return session.execute(sql).unique().all()
return sql.distinct().all()


def create_shapefile(things: list, filename: str = "things.shp") -> None:
Expand All @@ -97,7 +60,9 @@ def create_shapefile(things: list, filename: str = "things.shp") -> None:
shp.field("id", "L")
shp.field("name", "C")

for thing, point, elevation in things:
for thing in things:
point = thing.current_location.point
elevation = thing.current_location.elevation
# Assume loc.point is WKT or a Shapely geometry or GeoJSON
if isinstance(point, str):
try:
Expand Down
Loading