diff --git a/db/dbinfo/organization.bob.go b/db/dbinfo/organization.bob.go index 3a0089c2..e43b426f 100644 --- a/db/dbinfo/organization.bob.go +++ b/db/dbinfo/organization.bob.go @@ -258,6 +258,24 @@ var Organizations = Table[ Generated: true, AutoIncr: false, }, + ServiceAreaCentroidX: column{ + Name: "service_area_centroid_x", + DBType: "double precision", + Default: "GENERATED", + Comment: "", + Nullable: true, + Generated: true, + AutoIncr: false, + }, + ServiceAreaCentroidY: column{ + Name: "service_area_centroid_y", + DBType: "double precision", + Default: "GENERATED", + Comment: "", + Nullable: true, + Generated: true, + AutoIncr: false, + }, }, Indexes: organizationIndexes{ OrganizationPkey: index{ @@ -372,11 +390,13 @@ type organizationColumns struct { ServiceAreaXmax column ServiceAreaYmax column ServiceAreaCentroidGeojson column + ServiceAreaCentroidX column + ServiceAreaCentroidY column } func (c organizationColumns) AsSlice() []column { return []column{ - c.ID, c.Name, c.ArcgisID, c.ArcgisName, c.FieldseekerURL, c.ImportDistrictGid, c.Website, c.LogoUUID, c.Slug, c.GeneralManagerName, c.MailingAddressCity, c.MailingAddressPostalCode, c.MailingAddressStreet, c.OfficeAddressCity, c.OfficeAddressPostalCode, c.OfficeAddressStreet, c.ServiceAreaGeometry, c.ServiceAreaSquareMeters, c.ServiceAreaCentroid, c.ServiceAreaExtent, c.OfficeFax, c.OfficePhone, c.ServiceAreaXmin, c.ServiceAreaYmin, c.ServiceAreaXmax, c.ServiceAreaYmax, c.ServiceAreaCentroidGeojson, + c.ID, c.Name, c.ArcgisID, c.ArcgisName, c.FieldseekerURL, c.ImportDistrictGid, c.Website, c.LogoUUID, c.Slug, c.GeneralManagerName, c.MailingAddressCity, c.MailingAddressPostalCode, c.MailingAddressStreet, c.OfficeAddressCity, c.OfficeAddressPostalCode, c.OfficeAddressStreet, c.ServiceAreaGeometry, c.ServiceAreaSquareMeters, c.ServiceAreaCentroid, c.ServiceAreaExtent, c.OfficeFax, c.OfficePhone, c.ServiceAreaXmin, c.ServiceAreaYmin, c.ServiceAreaXmax, c.ServiceAreaYmax, c.ServiceAreaCentroidGeojson, c.ServiceAreaCentroidX, c.ServiceAreaCentroidY, } } diff --git a/db/factory/bobfactory_main.bob.go b/db/factory/bobfactory_main.bob.go index 2c7712dd..4c3eb2b5 100644 --- a/db/factory/bobfactory_main.bob.go +++ b/db/factory/bobfactory_main.bob.go @@ -2929,6 +2929,8 @@ func (f *Factory) FromExistingOrganization(m *models.Organization) *Organization o.ServiceAreaXmax = func() null.Val[float64] { return m.ServiceAreaXmax } o.ServiceAreaYmax = func() null.Val[float64] { return m.ServiceAreaYmax } o.ServiceAreaCentroidGeojson = func() null.Val[string] { return m.ServiceAreaCentroidGeojson } + o.ServiceAreaCentroidX = func() null.Val[float64] { return m.ServiceAreaCentroidX } + o.ServiceAreaCentroidY = func() null.Val[float64] { return m.ServiceAreaCentroidY } ctx := context.Background() if len(m.R.EmailContacts) > 0 { diff --git a/db/factory/organization.bob.go b/db/factory/organization.bob.go index fca80892..865382c8 100644 --- a/db/factory/organization.bob.go +++ b/db/factory/organization.bob.go @@ -65,6 +65,8 @@ type OrganizationTemplate struct { ServiceAreaXmax func() null.Val[float64] ServiceAreaYmax func() null.Val[float64] ServiceAreaCentroidGeojson func() null.Val[string] + ServiceAreaCentroidX func() null.Val[float64] + ServiceAreaCentroidY func() null.Val[float64] r organizationR f *Factory @@ -971,6 +973,12 @@ func (o OrganizationTemplate) Build() *models.Organization { if o.ServiceAreaCentroidGeojson != nil { m.ServiceAreaCentroidGeojson = o.ServiceAreaCentroidGeojson() } + if o.ServiceAreaCentroidX != nil { + m.ServiceAreaCentroidX = o.ServiceAreaCentroidX() + } + if o.ServiceAreaCentroidY != nil { + m.ServiceAreaCentroidY = o.ServiceAreaCentroidY() + } o.setModelRels(m) @@ -1902,6 +1910,8 @@ func (m organizationMods) RandomizeAllColumns(f *faker.Faker) OrganizationMod { OrganizationMods.RandomServiceAreaXmax(f), OrganizationMods.RandomServiceAreaYmax(f), OrganizationMods.RandomServiceAreaCentroidGeojson(f), + OrganizationMods.RandomServiceAreaCentroidX(f), + OrganizationMods.RandomServiceAreaCentroidY(f), } } @@ -3292,6 +3302,112 @@ func (m organizationMods) RandomServiceAreaCentroidGeojsonNotNull(f *faker.Faker }) } +// Set the model columns to this value +func (m organizationMods) ServiceAreaCentroidX(val null.Val[float64]) OrganizationMod { + return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) { + o.ServiceAreaCentroidX = func() null.Val[float64] { return val } + }) +} + +// Set the Column from the function +func (m organizationMods) ServiceAreaCentroidXFunc(f func() null.Val[float64]) OrganizationMod { + return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) { + o.ServiceAreaCentroidX = f + }) +} + +// Clear any values for the column +func (m organizationMods) UnsetServiceAreaCentroidX() OrganizationMod { + return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) { + o.ServiceAreaCentroidX = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +// The generated value is sometimes null +func (m organizationMods) RandomServiceAreaCentroidX(f *faker.Faker) OrganizationMod { + return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) { + o.ServiceAreaCentroidX = func() null.Val[float64] { + if f == nil { + f = &defaultFaker + } + + val := random_float64(f) + return null.From(val) + } + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +// The generated value is never null +func (m organizationMods) RandomServiceAreaCentroidXNotNull(f *faker.Faker) OrganizationMod { + return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) { + o.ServiceAreaCentroidX = func() null.Val[float64] { + if f == nil { + f = &defaultFaker + } + + val := random_float64(f) + return null.From(val) + } + }) +} + +// Set the model columns to this value +func (m organizationMods) ServiceAreaCentroidY(val null.Val[float64]) OrganizationMod { + return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) { + o.ServiceAreaCentroidY = func() null.Val[float64] { return val } + }) +} + +// Set the Column from the function +func (m organizationMods) ServiceAreaCentroidYFunc(f func() null.Val[float64]) OrganizationMod { + return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) { + o.ServiceAreaCentroidY = f + }) +} + +// Clear any values for the column +func (m organizationMods) UnsetServiceAreaCentroidY() OrganizationMod { + return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) { + o.ServiceAreaCentroidY = nil + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +// The generated value is sometimes null +func (m organizationMods) RandomServiceAreaCentroidY(f *faker.Faker) OrganizationMod { + return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) { + o.ServiceAreaCentroidY = func() null.Val[float64] { + if f == nil { + f = &defaultFaker + } + + val := random_float64(f) + return null.From(val) + } + }) +} + +// Generates a random value for the column using the given faker +// if faker is nil, a default faker is used +// The generated value is never null +func (m organizationMods) RandomServiceAreaCentroidYNotNull(f *faker.Faker) OrganizationMod { + return OrganizationModFunc(func(_ context.Context, o *OrganizationTemplate) { + o.ServiceAreaCentroidY = func() null.Val[float64] { + if f == nil { + f = &defaultFaker + } + + val := random_float64(f) + return null.From(val) + } + }) +} + func (m organizationMods) WithParentsCascading() OrganizationMod { return OrganizationModFunc(func(ctx context.Context, o *OrganizationTemplate) { if isDone, _ := organizationWithParentsCascadingCtx.Value(ctx); isDone { diff --git a/db/migrations/00067_org_centroid_coords.sql b/db/migrations/00067_org_centroid_coords.sql new file mode 100644 index 00000000..891e76a5 --- /dev/null +++ b/db/migrations/00067_org_centroid_coords.sql @@ -0,0 +1,3 @@ +-- +goose Up +ALTER TABLE organization ADD COLUMN service_area_centroid_x DOUBLE PRECISION GENERATED ALWAYS AS (ST_X(ST_Centroid(service_area_geometry))) STORED; +ALTER TABLE organization ADD COLUMN service_area_centroid_y DOUBLE PRECISION GENERATED ALWAYS AS (ST_Y(ST_Centroid(service_area_geometry))) STORED; diff --git a/db/models/organization.bob.go b/db/models/organization.bob.go index 59de9e1b..203fd879 100644 --- a/db/models/organization.bob.go +++ b/db/models/organization.bob.go @@ -56,6 +56,8 @@ type Organization struct { ServiceAreaXmax null.Val[float64] `db:"service_area_xmax,generated" ` ServiceAreaYmax null.Val[float64] `db:"service_area_ymax,generated" ` ServiceAreaCentroidGeojson null.Val[string] `db:"service_area_centroid_geojson,generated" ` + ServiceAreaCentroidX null.Val[float64] `db:"service_area_centroid_x,generated" ` + ServiceAreaCentroidY null.Val[float64] `db:"service_area_centroid_y,generated" ` R organizationR `db:"-" ` @@ -118,7 +120,7 @@ type organizationR struct { func buildOrganizationColumns(alias string) organizationColumns { return organizationColumns{ ColumnsExpr: expr.NewColumnsExpr( - "id", "name", "arcgis_id", "arcgis_name", "fieldseeker_url", "import_district_gid", "website", "logo_uuid", "slug", "general_manager_name", "mailing_address_city", "mailing_address_postal_code", "mailing_address_street", "office_address_city", "office_address_postal_code", "office_address_street", "service_area_geometry", "service_area_square_meters", "service_area_centroid", "service_area_extent", "office_fax", "office_phone", "service_area_xmin", "service_area_ymin", "service_area_xmax", "service_area_ymax", "service_area_centroid_geojson", + "id", "name", "arcgis_id", "arcgis_name", "fieldseeker_url", "import_district_gid", "website", "logo_uuid", "slug", "general_manager_name", "mailing_address_city", "mailing_address_postal_code", "mailing_address_street", "office_address_city", "office_address_postal_code", "office_address_street", "service_area_geometry", "service_area_square_meters", "service_area_centroid", "service_area_extent", "office_fax", "office_phone", "service_area_xmin", "service_area_ymin", "service_area_xmax", "service_area_ymax", "service_area_centroid_geojson", "service_area_centroid_x", "service_area_centroid_y", ).WithParent("organization"), tableAlias: alias, ID: psql.Quote(alias, "id"), @@ -148,6 +150,8 @@ func buildOrganizationColumns(alias string) organizationColumns { ServiceAreaXmax: psql.Quote(alias, "service_area_xmax"), ServiceAreaYmax: psql.Quote(alias, "service_area_ymax"), ServiceAreaCentroidGeojson: psql.Quote(alias, "service_area_centroid_geojson"), + ServiceAreaCentroidX: psql.Quote(alias, "service_area_centroid_x"), + ServiceAreaCentroidY: psql.Quote(alias, "service_area_centroid_y"), } } @@ -181,6 +185,8 @@ type organizationColumns struct { ServiceAreaXmax psql.Expression ServiceAreaYmax psql.Expression ServiceAreaCentroidGeojson psql.Expression + ServiceAreaCentroidX psql.Expression + ServiceAreaCentroidY psql.Expression } func (c organizationColumns) Alias() string { @@ -4449,6 +4455,8 @@ type organizationWhere[Q psql.Filterable] struct { ServiceAreaXmax psql.WhereNullMod[Q, float64] ServiceAreaYmax psql.WhereNullMod[Q, float64] ServiceAreaCentroidGeojson psql.WhereNullMod[Q, string] + ServiceAreaCentroidX psql.WhereNullMod[Q, float64] + ServiceAreaCentroidY psql.WhereNullMod[Q, float64] } func (organizationWhere[Q]) AliasedAs(alias string) organizationWhere[Q] { @@ -4484,6 +4492,8 @@ func buildOrganizationWhere[Q psql.Filterable](cols organizationColumns) organiz ServiceAreaXmax: psql.WhereNull[Q, float64](cols.ServiceAreaXmax), ServiceAreaYmax: psql.WhereNull[Q, float64](cols.ServiceAreaYmax), ServiceAreaCentroidGeojson: psql.WhereNull[Q, string](cols.ServiceAreaCentroidGeojson), + ServiceAreaCentroidX: psql.WhereNull[Q, float64](cols.ServiceAreaCentroidX), + ServiceAreaCentroidY: psql.WhereNull[Q, float64](cols.ServiceAreaCentroidY), } } diff --git a/platform/csv/pool.go b/platform/csv/pool.go index 35b80b5a..91ec0828 100644 --- a/platform/csv/pool.go +++ b/platform/csv/pool.go @@ -379,20 +379,29 @@ func errorMissingHeader(ctx context.Context, txn bob.Tx, c *models.FileuploadCSV return addError(ctx, txn, c, 0, 0, msg) } func maybeAddServiceArea(req *stadia.StructuredGeocodeRequest, org *models.Organization) { - if org.ServiceAreaXmax.IsNull() || - org.ServiceAreaYmax.IsNull() || - org.ServiceAreaXmin.IsNull() || - org.ServiceAreaYmin.IsNull() { + /* + if org.ServiceAreaXmax.IsNull() || + org.ServiceAreaYmax.IsNull() || + org.ServiceAreaXmin.IsNull() || + org.ServiceAreaYmin.IsNull() { + return + } + xmax := org.ServiceAreaXmax.MustGet() + ymax := org.ServiceAreaYmax.MustGet() + xmin := org.ServiceAreaXmin.MustGet() + ymin := org.ServiceAreaYmin.MustGet() + req.BoundaryRectMaxLon = &xmax + req.BoundaryRectMaxLat = &ymax + req.BoundaryRectMinLon = &xmin + req.BoundaryRectMinLat = &ymin + */ + if org.ServiceAreaCentroidX.IsNull() || org.ServiceAreaCentroidY.IsNull() { return } - xmax := org.ServiceAreaXmax.MustGet() - ymax := org.ServiceAreaYmax.MustGet() - xmin := org.ServiceAreaXmin.MustGet() - ymin := org.ServiceAreaYmin.MustGet() - req.BoundaryRectMaxLon = &xmax - req.BoundaryRectMaxLat = &ymax - req.BoundaryRectMinLon = &xmin - req.BoundaryRectMinLat = &ymin + centroid_x := org.ServiceAreaCentroidX.MustGet() + centroid_y := org.ServiceAreaCentroidY.MustGet() + req.FocusPointLat = ¢roid_y + req.FocusPointLng = ¢roid_x } func parseHeaders(row []string) ([]headerPoolEnum, []string) { result_enums := make([]headerPoolEnum, 0) diff --git a/stadia/cmd/structured-geocode/main.go b/stadia/cmd/structured-geocode/main.go index f66283da..56e53d62 100644 --- a/stadia/cmd/structured-geocode/main.go +++ b/stadia/cmd/structured-geocode/main.go @@ -16,6 +16,7 @@ func main() { boundaryRectMinLat := flag.Float64("boundary-rect-min-lat", 0, "The min lat of the boundary") boundaryRectMaxLon := flag.Float64("boundary-rect-max-lng", 0, "The max lon of the boundary") boundaryRectMinLon := flag.Float64("boundary-rect-min-lng", 0, "The min lon of the boundary") + city := flag.String("city", "", "City address to geocode") postalCode := flag.String("postal-code", "", "Postal code") focusLat := flag.Float64("focus-lat", 0, "The latitude of the focus point") focusLng := flag.Float64("focus-lng", 0, "The longitude of the focus point") @@ -35,23 +36,23 @@ func main() { flag.Usage() os.Exit(1) } - if focusLat != nil && focusLng == nil { + if *focusLat != 0 && *focusLng == 0 { log.Println("Error: you must specify both focus-lat and focus-lng together, not just focus-lat") flag.Usage() os.Exit(1) } - if focusLat == nil && focusLng != nil { + if *focusLat == 0 && *focusLng != 0 { log.Println("Error: you must specify both focus-lat and focus-lng together, not just focus-lng") flag.Usage() os.Exit(1) } - if (boundaryRectMaxLat != nil || - boundaryRectMinLat != nil || - boundaryRectMaxLon != nil || - boundaryRectMinLon != nil) && (boundaryRectMaxLat == nil || - boundaryRectMinLat == nil || - boundaryRectMaxLon == nil || - boundaryRectMinLon == nil) { + if (*boundaryRectMaxLat != 0 || + *boundaryRectMinLat != 0 || + *boundaryRectMaxLon != 0 || + *boundaryRectMinLon != 0) && (*boundaryRectMaxLat == 0 || + *boundaryRectMinLat == 0 || + *boundaryRectMaxLon == 0 || + *boundaryRectMinLon == 0) { log.Println("If you specify one of boundary-rect you need to specify them all") os.Exit(1) } @@ -68,16 +69,19 @@ func main() { Address: address, PostalCode: postalCode, } - if focusLat != nil && focusLng != nil { + if *focusLat != 0 && *focusLng != 0 { req.FocusPointLat = focusLat req.FocusPointLng = focusLng } - if boundaryRectMaxLat != nil { + if *boundaryRectMaxLat != 0 { req.BoundaryRectMaxLat = boundaryRectMaxLat req.BoundaryRectMinLat = boundaryRectMinLat req.BoundaryRectMaxLon = boundaryRectMaxLon req.BoundaryRectMinLon = boundaryRectMinLon } + if *city != "" { + req.Locality = city + } resp, err := client.StructuredGeocode(ctx, req) if err != nil { log.Printf("err: %v\n", err) @@ -86,7 +90,7 @@ func main() { log.Printf("type: %s, features: %d\n", resp.Type, len(resp.Features)) for i, feature := range resp.Features { log.Printf("feature %d: type %s\n", i, feature.Type) - log.Printf("\tgeometry %s\n", feature.Geometry.Type) + log.Printf("\tgeometry %s (%f %f)\n", feature.Geometry.Type, feature.Geometry.Coordinates[0], feature.Geometry.Coordinates[1]) log.Printf("\tproperties %s\n", feature.Properties.Layer) } } diff --git a/stadia/error.go b/stadia/error.go new file mode 100644 index 00000000..6a58e5b5 --- /dev/null +++ b/stadia/error.go @@ -0,0 +1,80 @@ +package stadia + +import ( + "encoding/json" + "fmt" + "io" + + "resty.dev/v3" +) + +// Unfortunately, Stadia Maps is inconsistent in how it handles errors. +// We therefore have to have a function that handles all the different JSON +// error variations. +func parseError(resp *resty.Response) error { + content, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("reading all body: %w", err) + } + var server_error serverError + err = json.Unmarshal(content, &server_error) + if err == nil { + return newAPIError(resp.StatusCode(), server_error.Error.Reason) + } + + // At this point we've exhausted all of our options, so just pass the JSON through + return newAPIError(resp.StatusCode(), string(content)) +} + +type apiError struct { + Message string + Status int +} + +func newAPIError(status int, msg string) apiError { + return apiError{ + Message: msg, + Status: status, + } +} +func (e apiError) Error() string { + return e.Message +} + +type Error struct { + ErrorMessage string `json:"error"` + Errors []string `json:"errors"` +} + +func (e *Error) Error() string { + return e.ErrorMessage +} + +/* +Got this when I managed to bork the server + + { + "error": { + "reason": "Internal Server Error" + }, + "status": 500 + } +*/ +type errorWithReason struct { + Reason string `json:"reason"` +} +type serverError struct { + Error errorWithReason `json:"error"` + Status int `json:"status"` +} + +/* + if len(result.Geocode.Errors) > 0 { + joined := strings.Join(result.Geocode.Errors, ", ") + return nil, fmt.Errorf("structured geocoding failure: %d '%s'", resp.StatusCode(), joined) + } else if result.Geocode.Error != "" { + return nil, fmt.Errorf("structured geocoding failure: %d '%s'", resp.StatusCode(), result.Geocode.Error) + } else { + return nil, fmt.Errorf("structured geocoding failure: %d", resp.StatusCode()) + } +*/ diff --git a/stadia/response_type.go b/stadia/response_type.go index 14cc9d46..9ef85b9b 100644 --- a/stadia/response_type.go +++ b/stadia/response_type.go @@ -1,18 +1,9 @@ package stadia -type Error struct { - ErrorMessage string `json:"error"` - Errors []string `json:"errors"` -} - -func (e *Error) Error() string { - return e.ErrorMessage -} - // GeocodeResponse represents the top-level response from the geocoding API type GeocodeResponse struct { BBox []float64 `json:"bbox"` // [W, S, E, N] - ErrorMessage string `json:"error"` + ErrorMessage string `json:"error,omitempty"` Features []GeocodeFeature `json:"features"` Geocode GeocodeMeta `json:"geocoding"` Type string `json:"type"` // Should be "FeatureCollection" diff --git a/stadia/structured_geocode.go b/stadia/structured_geocode.go index 622736c9..91d41227 100644 --- a/stadia/structured_geocode.go +++ b/stadia/structured_geocode.go @@ -3,7 +3,6 @@ package stadia import ( "context" "fmt" - "strings" "github.com/google/go-querystring/query" ) @@ -58,7 +57,6 @@ func (s *StadiaMaps) StructuredGeocode(ctx context.Context, req StructuredGeocod resp, err := s.client.R(). SetQueryParamsFromValues(query). SetContext(ctx). - SetError(&result). SetResult(&result). SetPathParam("urlBase", s.urlBase). SetQueryParam("api_key", s.APIKey). @@ -68,24 +66,7 @@ func (s *StadiaMaps) StructuredGeocode(ctx context.Context, req StructuredGeocod } if !resp.IsSuccess() { - /* - if api_error.Error() != "" { - return nil, &api_error - } - content, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("read all failure: %w", err) - } - */ - fmt.Printf("geocoding error: %s\n", result.Geocode.Error) - if len(result.Geocode.Errors) > 0 { - joined := strings.Join(result.Geocode.Errors, ", ") - return nil, fmt.Errorf("structured geocoding failure: %d '%s'", resp.StatusCode(), joined) - } else if result.Geocode.Error != "" { - return nil, fmt.Errorf("structured geocoding failure: %d '%s'", resp.StatusCode(), result.Geocode.Error) - } else { - return nil, fmt.Errorf("structured geocoding failure: %d", resp.StatusCode()) - } + return nil, parseError(resp) } return &result, nil }