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
19 changes: 19 additions & 0 deletions core/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,25 @@
SRID_UTM_ZONE_13N = 26913
SRID_UTM_ZONE_12N = 26912

# EPSG 269xx == NAD83 / UTM zone xxN, but only for xx = 01..23; 26924-26928
# don't exist, and 26929+ names unrelated NAD83 state-plane systems. Safe here
# because AMP's 10N-19N range sits well inside 1-23.
SRID_NAD83_UTM_BASE = 26900

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Small doc accuracy point: the comment says "EPSG 269xx == NAD83 / UTM zone xxN," which holds for 26901–26923 but not past it. 26924–26928 don't exist, and 26929+ are NAD83 state plane (26929 = Alabama East). Worth stating the 1–23 bound here since this constant is what makes the arithmetic possible, and the bound is the reason the range check downstream isn't optional.


# AMP water-well ingestion policy: submissions are limited to the continental
# US. Not a projection limit -- domain/geospatial.py serves Location points
# stored anywhere on earth and must not import these; that coupling is what
# let a CONUS bound apply to the worldwide read path once before.
AMP_UTM_ZONE_MIN = 10
AMP_UTM_ZONE_MAX = 19

# A coarse sanity range, not a national border: it catches transposed
# easting/northing and feet-vs-meters entry mistakes on AMP well submissions.
# It is CONUS-shaped, not global -- e.g. it excludes the entire eastern
# hemisphere -- which is fine for this policy but wrong for anything else.
AMP_COORD_LAT_MIN, AMP_COORD_LAT_MAX = 18.0, 72.0
AMP_COORD_LON_MIN, AMP_COORD_LON_MAX = -180.0, -66.0

STATE_CODES = (
"AL",
"AK",
Expand Down
95 changes: 95 additions & 0 deletions domain/geospatial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# ===============================================================================
# Copyright 2026 ross
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
"""
UTM zone/CRS lookup for a stored point, used on the read path.

``domain/wells.py`` parses a UTM zone label supplied on import, scoped to
AMP's CONUS-only water-well ingestion policy. This module is the read-side
counterpart and has no such scope: a ``Location`` can be stored anywhere on
earth, so this resolves the true WGS84 UTM zone for any point against
pyproj's CRS database rather than computing an EPSG code, and raises
``OutsideUtmDomain`` for the polar latitudes UTM doesn't cover. See
``schemas/location.py``.

Deliberately does not import ``AMP_UTM_ZONE_MIN/MAX`` or ``AMP_COORD_*``:
those are AMP ingestion policy, not a projection limit, and importing them
here is exactly the coupling that let a CONUS bound silently clamp this
worldwide path before.
"""

from functools import lru_cache

from pyproj.aoi import AreaOfInterest
from pyproj.database import query_utm_crs_info

# UTM is undefined outside this band; polar points need UPS/Polar
# Stereographic (EPSG 32661/32761 or 3413/3031), which is not implemented
# here. Raising rather than clamping keeps that gap visible.
UTM_LAT_MIN, UTM_LAT_MAX = -80.0, 84.0


class OutsideUtmDomain(ValueError):
"""Raised when a point falls outside the latitude band UTM is defined for."""


def utm_zone_for_longitude(longitude: float) -> int:

@jirhiker jirhiker Aug 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Amended after clarifying scope with Jake. Ocotillo should store locations anywhere on earth; AMP water-well ingestion should stay CONUS-only. Taking longitude only forecloses two things this path now needs, since it has to serve any stored point:

  • Hemisphere. N vs S can't be derived from longitude, so this can never label a southern-hemisphere point. (The mirror assumption in schemas/well_inventory.py is fine — that path is CONUS by policy. This one isn't.)
  • Latitude bounds. UTM is undefined outside 80°S–84°N, and nothing in the codebase checks that today.

Suggest (longitude, latitude) — the call site in schemas/location.py already has the full point in hand, so it costs one argument and lets the polar branch slot in later without touching callers.

Separate from all of the above, the zone math has an edge case the clamp is currently hiding: int((longitude + 180) // 6) + 1 returns 61 at longitude == 180.0, and unnormalized input (185, -190) yields 62 and −2. So removing the clamp surfaces a crash unless this changes too. int(((longitude + 180) % 360) // 6) + 1 holds 1–60 across the range I tested (−190 … 185).

"""
Return the UTM zone number (1-60) for a longitude.

UTM zones are 6 degrees wide starting at -180. The longitude is
normalized onto [-180, 180) first so values at or past the antimeridian
(180.0, 185, -190, ...) still land in range instead of returning 61, 62,
or a negative zone.
"""
return int(((longitude + 180) % 360) // 6) + 1


@lru_cache(maxsize=128)
def _utm_epsg(zone: int, northern: bool) -> int:
"""Look up the WGS84 UTM EPSG code for a zone/hemisphere pair."""
# Resolved against the EPSG database rather than computed, so a zone that
# doesn't exist raises instead of landing on an unrelated CRS.
lon = (zone - 1) * 6 - 177 # zone central meridian
lat = 1.0 if northern else -1.0
suffix = f"{zone}{'N' if northern else 'S'}"
for info in query_utm_crs_info(
datum_name="WGS 84",
area_of_interest=AreaOfInterest(lon, lat, lon, lat),
):
if info.name.endswith(suffix):
return int(info.code)
raise OutsideUtmDomain(f"No WGS 84 UTM CRS for zone {suffix}")


def utm_crs_for_point(longitude: float, latitude: float) -> tuple[int, str]:
"""
Return the ``(EPSG code, zone label)`` of the UTM zone containing a point.

Hemisphere is derived from latitude, not assumed, since this path serves
points anywhere on earth (unlike the AMP importer, which only ever
accepts northern-hemisphere zones by policy).
"""
if not UTM_LAT_MIN <= latitude <= UTM_LAT_MAX:
raise OutsideUtmDomain(
f"Latitude {latitude} is outside the UTM domain "
f"({UTM_LAT_MIN} to {UTM_LAT_MAX}); polar points need UPS."
)
zone = utm_zone_for_longitude(longitude)
northern = latitude >= 0
return _utm_epsg(zone, northern), f"{zone}{'N' if northern else 'S'}"


# ============= EOF =============================================
38 changes: 27 additions & 11 deletions domain/wells.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

import re

from core.constants import SRID_UTM_ZONE_12N, SRID_UTM_ZONE_13N
from core.constants import AMP_UTM_ZONE_MAX, AMP_UTM_ZONE_MIN, SRID_NAD83_UTM_BASE
from domain.units import convert_ft_to_m
from domain.values import enum_value

Expand All @@ -37,12 +37,7 @@
r"^(?P<prefix>[A-Z]{2,3})\s*-\s*(?:x{4}|X{4})$", re.IGNORECASE
)

# TODO: this needs to be more sophisticated in the future. Likely more than 13N
# and 12N will be used.
UTM_ZONE_SRIDS = {
"13N": SRID_UTM_ZONE_13N,
"12N": SRID_UTM_ZONE_12N,
}
UTM_ZONE_REGEX = re.compile(r"^\s*(\d{1,2})\s*N\s*$", re.IGNORECASE)

@jirhiker jirhiker Aug 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Amended after clarifying scope with Jake. Ocotillo should store locations anywhere on earth; AMP water-well ingestion should stay CONUS-only. Retracting the main point I made here. I'd suggested widening this to [NS] for southern-hemisphere support. That's wrong for this module — domain/wells.py serves AMP water-well ingestion, which is CONUS by policy, so rejecting an S suffix outright is correct and should stay. Same for the 10N–19N bound below.

The one thing I'd still keep from the original comment is much smaller: the rejection message. Unsupported UTM zone: 13S gives the submitter no signal about whether the problem is the 13 or the S. Since the range is now a deliberate policy rather than an implementation limit, it's worth saying so — something like "southern-hemisphere zones are not accepted; AMP well submissions are limited to CONUS zones 10N-19N."

\d{1,2} admitting up to 99 is fine given the range check downstream. Not worth changing.


RELEASE_STATUS_PUBLIC = "public"
RELEASE_STATUS_PRIVATE = "private"
Expand Down Expand Up @@ -91,12 +86,33 @@ def autogen_prefix(well_id: str | None) -> str | None:
return None


def utm_zone_number(utm_zone: str | None) -> int:
"""
Parse a UTM zone label (e.g. ``"13N"``) into its zone number.

Accepts any northern-hemisphere zone from ``AMP_UTM_ZONE_MIN`` to
``AMP_UTM_ZONE_MAX`` (10N-19N spans the continental US), case-insensitively
and tolerant of surrounding whitespace. This is AMP water-well ingestion
policy, not a projection limit -- see domain/geospatial.py for the
worldwide read path. Anything else -- a bad shape, a southern-hemisphere
suffix, or a zone outside that range -- raises ``UnsupportedUtmZone``.
"""
match = UTM_ZONE_REGEX.match(utm_zone or "")
if match:
zone = int(match.group(1))
if AMP_UTM_ZONE_MIN <= zone <= AMP_UTM_ZONE_MAX:
return zone

raise UnsupportedUtmZone(
f"Unsupported UTM zone: {utm_zone}. AMP well submissions are limited "
f"to CONUS zones {AMP_UTM_ZONE_MIN}N-{AMP_UTM_ZONE_MAX}N "
f"(southern-hemisphere zones are not accepted)."
)


def srid_for_utm_zone(utm_zone: str | None) -> int:
"""Return the EPSG code for a supported UTM zone label."""
try:
return UTM_ZONE_SRIDS[utm_zone]
except KeyError:
raise UnsupportedUtmZone(f"Unsupported UTM zone: {utm_zone}") from None
return SRID_NAD83_UTM_BASE + utm_zone_number(utm_zone)


def elevation_m_from_ft(elevation_ft: float | str | None) -> float:
Expand Down
40 changes: 25 additions & 15 deletions schemas/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
from geoalchemy2.shape import to_shape
from pydantic import BaseModel, model_validator, field_validator, Field, ConfigDict

from core.constants import SRID_WGS84, SRID_UTM_ZONE_13N
from core.constants import SRID_WGS84
from core.enums import ElevationMethod, CoordinateMethod
from core.enums import ReleaseStatus
from domain.geospatial import OutsideUtmDomain, utm_crs_for_point
from schemas import BaseCreateModel, BaseUpdateModel, BaseResponseModel
from schemas.notes import NoteResponse, CreateNote, UpdateNote
from services.util import convert_m_to_ft, transform_srid
Expand Down Expand Up @@ -88,8 +89,8 @@ class GeoJSONGeometry(BaseModel):
class GeoJSONUTMCoordinates(BaseModel):
easting: float
northing: float
utm_zone: str = "13N"
horizontal_datum: str = "NAD83"
utm_zone: str
horizontal_datum: str = "WGS84"

model_config = ConfigDict(
from_attributes=True,
Expand All @@ -105,9 +106,9 @@ class GeoJSONProperties(BaseModel):
county: str | None = None
state: str | None = None
quad_name: str | None = None
utm_coordinates: GeoJSONUTMCoordinates = Field(
default_factory=GeoJSONUTMCoordinates
)
# None for points outside the UTM domain (poles) -- there is no valid
# zone to report, so the field is omitted rather than given a fake one.
utm_coordinates: GeoJSONUTMCoordinates | None = None
notes: list[NoteResponse] = []
nma_location_notes: str | None = None
nma_data_reliability: str | None = None
Expand Down Expand Up @@ -154,7 +155,6 @@ def populate_fields(cls, data: Any) -> Any:
# add empty fields as necessary
data_dict["geometry"] = {}
data_dict["properties"] = {}
data_dict["properties"]["utm_coordinates"] = {}

# populate coordinates
point_wgs84_wkb = data_dict.get("point")
Expand All @@ -180,14 +180,24 @@ def populate_fields(cls, data: Any) -> Any:
data_dict["properties"]["nma_date_created"] = data_dict.get("nma_date_created")
data_dict["properties"]["nma_site_date"] = data_dict.get("nma_site_date")

# populate UTM coordinates
point_utm_zone_13n_wkt = transform_srid(
point_wgs84_wkt, SRID_WGS84, SRID_UTM_ZONE_13N
)
data_dict["properties"]["utm_coordinates"]["easting"] = point_utm_zone_13n_wkt.x
data_dict["properties"]["utm_coordinates"][
"northing"
] = point_utm_zone_13n_wkt.y
# Populate UTM coordinates using the zone/hemisphere the point actually
# falls in, not a fixed NM zone -- a well outside 12N/13N previously
# read back with a confidently wrong easting/northing labeled "13N".
# A point outside the UTM domain (poles) has no valid zone to report;
# the geometry member already carries WGS84 lon/lat for those, so the
# block is omitted rather than given a fake label.
try:
srid, zone_label = utm_crs_for_point(point_wgs84_wkt.x, point_wgs84_wkt.y)
except OutsideUtmDomain:
data_dict["properties"]["utm_coordinates"] = None
else:
point_utm_wkt = transform_srid(point_wgs84_wkt, SRID_WGS84, srid)
data_dict["properties"]["utm_coordinates"] = {
"easting": point_utm_wkt.x,
"northing": point_utm_wkt.y,
"utm_zone": zone_label,
"horizontal_datum": "WGS84",
}

return data_dict

Expand Down
40 changes: 29 additions & 11 deletions schemas/well_inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@

import phonenumbers
import utm
from core.constants import STATE_CODES
from core.constants import (
AMP_COORD_LAT_MAX,
AMP_COORD_LAT_MIN,
AMP_COORD_LON_MAX,
AMP_COORD_LON_MIN,
STATE_CODES,
)
from core.enums import (
ElevationMethod,
Role,
Expand Down Expand Up @@ -47,6 +53,7 @@
Field,
AliasChoices,
)
from domain.wells import utm_zone_number
from schemas import past_or_today_validator, PastOrTodayDatetime
from services.util import normalize_datetime_to_utc

Expand Down Expand Up @@ -375,22 +382,33 @@ def normalize_measurement_date_time(cls, value: datetime | None) -> datetime | N
return None
return normalize_datetime_to_utc(value)

@field_validator("utm_zone", mode="after")
@classmethod
def normalize_utm_zone(cls, value: str) -> str:
# Canonicalize so downstream callers (services/well_inventory_csv.py) see
# the same string this validator checked -- a stray "13n" used to pass
# here but fail case-sensitively at persist time.
return (value or "").strip().upper()

@model_validator(mode="after")
def validate_model(self):
# verify utm in NM
utm_zone_value = (self.utm_zone or "").upper()
if utm_zone_value not in ("12N", "13N"):
raise ValueError("Invalid utm zone. Must be one of: 12N, 13N")

zone = int(utm_zone_value[:-1])
northern = True # only northern hemisphere zones (12N, 13N) are supported
# utm_zone_number raises UnsupportedUtmZone (a ValueError) for anything
# outside the supported northern-hemisphere CONUS range; Pydantic wraps it
# with its own "Value error, " prefix, so no message is composed here.
zone = utm_zone_number(self.utm_zone)
northern = True # utm_zone_number rejects anything but an "N" suffix

@jirhiker jirhiker Aug 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Amended after clarifying scope with Jake. Ocotillo should store locations anywhere on earth; AMP water-well ingestion should stay CONUS-only. Softening this one. northern = True is not a latent assumption on this path — AMP ingestion is CONUS by policy, so it's permanently correct. The comment explaining it is accurate. Nothing to change.

The observation about test_badly_scaled_coordinates_raise_out_of_range_error still holds, though, as a note on what's actually guaranteeing correctness here. It's true that utm.to_latlon rejects an out-of-domain easting, but the library validates northing range and zone number, never latitude:

utm.to_latlon(500000, 500000, 58, northern=False) -> lat -85.540   no error
utm.to_latlon(500000,      0, 58, northern=False) -> lat -90.018   no error

That last return value isn't a latitude. Not reachable through this importer — the CONUS coordinate range gates it well before that — so this is informational rather than a request. Just worth knowing that the bounding box, not the library, is doing the real work, in case the box is ever loosened.

lat, lon = utm.to_latlon(
self.utm_easting, self.utm_northing, zone, northern=northern
)
if not ((31.33 <= lat <= 37.00) and (-109.05 <= lon <= -103.00)):
# A coarse sanity check, not a border check -- catches transposed
# easting/northing and feet-vs-meters entry mistakes.
if not (
AMP_COORD_LAT_MIN <= lat <= AMP_COORD_LAT_MAX
and AMP_COORD_LON_MIN <= lon <= AMP_COORD_LON_MAX
):
raise ValueError(
f"UTM coordinates are outside of the NM. E={self.utm_easting} N={self.utm_northing}"
f" Zone={self.utm_zone}"
f"UTM coordinates are outside the expected range. E={self.utm_easting}"
f" N={self.utm_northing} Zone={self.utm_zone}"
)

if self.depth_to_water_ft is not None:
Expand Down
2 changes: 1 addition & 1 deletion tests/features/data/well-inventory-invalid-utm.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
project,well_name_point_id,site_name,date_time,field_staff,utm_easting,utm_northing,utm_zone,elevation_ft,elevation_method,measuring_point_height_ft,field_staff_2,field_staff_3,contact_1_name,contact_1_organization,contact_1_role,contact_1_type,contact_1_phone_1,contact_1_phone_1_type,contact_1_phone_2,contact_1_phone_2_type,contact_1_email_1,contact_1_email_1_type,contact_1_email_2,contact_1_email_2_type,contact_1_address_1_line_1,contact_1_address_1_line_2,contact_1_address_1_type,contact_1_address_1_state,contact_1_address_1_city,contact_1_address_1_postal_code,contact_1_address_2_line_1,contact_1_address_2_line_2,contact_1_address_2_type,contact_1_address_2_state,contact_1_address_2_city,contact_1_address_2_postal_code,contact_2_name,contact_2_organization,contact_2_role,contact_2_type,contact_2_phone_1,contact_2_phone_1_type,contact_2_phone_2,contact_2_phone_2_type,contact_2_email_1,contact_2_email_1_type,contact_2_email_2,contact_2_email_2_type,contact_2_address_1_line_1,contact_2_address_1_line_2,contact_2_address_1_type,contact_2_address_1_state,contact_2_address_1_city,contact_2_address_1_postal_code,contact_2_address_2_line_1,contact_2_address_2_line_2,contact_2_address_2_type,contact_2_address_2_state,contact_2_address_2_city,contact_2_address_2_postal_code,directions_to_site,specific_location_of_well,repeat_measurement_permission,sampling_permission,datalogger_installation_permission,public_availability_acknowledgement,result_communication_preference,contact_special_requests_notes,ose_well_record_id,date_drilled,completion_source,total_well_depth_ft,historic_depth_to_water_ft,depth_source,well_pump_type,well_pump_depth_ft,is_open,datalogger_possible,casing_diameter_ft,measuring_point_description,well_purpose,well_purpose_2,well_status,monitoring_frequency,sampling_scenario_notes,well_measuring_notes,sample_possible
Middle Rio Grande Groundwater Monitoring,MRG-001_MP1,Smith Farm Domestic Well,2025-02-15T10:30:00,A Lopez,457100,4159020,13N,5250,Survey-grade GPS,1.5,B Chen,,John Smith,NMBGMR,Owner,Primary,505-555-0101,Primary,,,john.smith@example.com,Primary,,,123 County Rd 7,,Mailing,NM,Los Lunas,87031,,,,,,,Maria Garcia,NMBGMR,Principal Investigator,Secondary,505-555-0123,Home,,,maria.garcia@mrgcd.nm.gov,Work,,,1931 2nd St SW,Suite 200,Mailing,NM,Albuquerque,87102,,,,,,,Gate off County Rd 7 0.4 miles south of canal crossing,Domestic well in pump house east of residence,True,True,True,True,email,Call before visits during irrigation season,OSE-123456,2010-06-15,Interpreted fr geophys logs by source agency,280,45,"Memory of owner, operator, driller",Submersible,200,True,True,0.5,Top of steel casing inside pump house marked with orange paint,Domestic,,"Active, pumping well",Biannual,Sample only when pump has been off more than 12 hours,Measure before owner starts irrigation,True
Middle Rio Grande Groundwater Monitoring,MRG-001_MP1,Smith Farm Domestic Well,2025-02-15T10:30:00,A Lopez,500000,1000000,13N,5250,Survey-grade GPS,1.5,B Chen,,John Smith,NMBGMR,Owner,Primary,505-555-0101,Primary,,,john.smith@example.com,Primary,,,123 County Rd 7,,Mailing,NM,Los Lunas,87031,,,,,,,Maria Garcia,NMBGMR,Principal Investigator,Secondary,505-555-0123,Home,,,maria.garcia@mrgcd.nm.gov,Work,,,1931 2nd St SW,Suite 200,Mailing,NM,Albuquerque,87102,,,,,,,Gate off County Rd 7 0.4 miles south of canal crossing,Domestic well in pump house east of residence,True,True,True,True,email,Call before visits during irrigation season,OSE-123456,2010-06-15,Interpreted fr geophys logs by source agency,280,45,"Memory of owner, operator, driller",Submersible,200,True,True,0.5,Top of steel casing inside pump house marked with orange paint,Domestic,,"Active, pumping well",Biannual,Sample only when pump has been off more than 12 hours,Measure before owner starts irrigation,True
Middle Rio Grande Groundwater Monitoring,MRG-003_MP1,Old Orchard Well,2025-01-20T09:00:00,B Chen,250000,4000000,13S,5320,Global positioning system (GPS),1.8,,,Emily Davis,NMBGMR,Biologist,Primary,505-555-0303,Work,,,emily.davis@example.org,Work,,,78 Orchard Ln,,Mailing,NM,Los Lunas,87031,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,From Main St turn east on Orchard Ln well house at dead end,Abandoned irrigation well in small cinderblock building,False,False,False,True,phone,Owner prefers weekday visits,,1965-04-10,From driller's log or well report,350,60,"Reported by person other than driller owner agency",Jet,280,False,False,0.75,Top of steel casing under removable hatch use fixed reference mark,Irrigation,,Abandoned,Annual,Sampling not permitted water level only when owner present,Well house can be locked coordinate ahead,False
Middle Rio Grande Groundwater Monitoring,MRG-005_MP1,Valid Well,2025-02-15T10:30:00,A Lopez,250000,4000000,13N,5250,Survey-grade GPS,1.5,B Chen,,John Smith,NMBGMR,Owner,Primary,505-555-0101,Primary,,,john.smith@example.com,Primary,,,123 County Rd 7,,Mailing,NM,Los Lunas,87031,,,,,,,Maria Garcia,NMBGMR,Principal Investigator,Secondary,505-555-0123,Home,,,maria.garcia@mrgcd.nm.gov,Work,,,1931 2nd St SW,Suite 200,Mailing,NM,Albuquerque,87102,,,,,,,Gate off County Rd 7 0.4 miles south of canal crossing,Domestic well in pump house east of residence,True,True,True,True,email,Call before visits during irrigation season,OSE-123456,2010-06-15,Interpreted fr geophys logs by source agency,280,45,"Memory of owner, operator, driller",Submersible,200,True,True,0.5,Top of steel casing inside pump house marked with orange paint,Domestic,,"Active, pumping well",Biannual,Sample only when pump has been off more than 12 hours,Measure before owner starts irrigation,True
10 changes: 6 additions & 4 deletions tests/features/steps/well-core-information.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from behave import then
from core.constants import SRID_WGS84, SRID_UTM_ZONE_13N
from core.constants import SRID_WGS84
from geoalchemy2.shape import to_shape
from services.util import (
transform_srid,
Expand Down Expand Up @@ -278,7 +278,7 @@ def step_step_step_6(context):


@then(
"the response should include the UTM coordinates with datum NAD83 in the properties"
"the response should include the UTM coordinates with datum WGS84 in the properties"
)
def step_step_step_7(context):

Expand All @@ -288,15 +288,17 @@ def step_step_step_7(context):

point_wkb = context.objects["locations"][0].point
point_wkt = to_shape(point_wkb)
point_utm_zone_13 = transform_srid(point_wkt, SRID_WGS84, SRID_UTM_ZONE_13N)
# Fixture location is genuinely zone 13N; the read path resolves this via
# WGS84 UTM (EPSG 32613), not the NAD83 code (26913) it used to assume.
point_utm_zone_13 = transform_srid(point_wkt, SRID_WGS84, 32613)

assert context.water_well_data["current_location"]["properties"][
"utm_coordinates"
] == {
"easting": point_utm_zone_13.x,
"northing": point_utm_zone_13.y,
"utm_zone": "13N",
"horizontal_datum": "NAD83",
"horizontal_datum": "WGS84",
}


Expand Down
Loading
Loading