Skip to content

feat: New endpoint for shapefile data #82

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 22, 2025
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
192 changes: 192 additions & 0 deletions bom-shapefiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"log"
"net/http"
"strconv"
"strings"
"time"
)

// ParishShpHandler returns a GeoJSON FeatureCollection containing parish
Expand Down Expand Up @@ -96,3 +98,193 @@ func (s *Server) ParishShpHandler() http.HandlerFunc {
fmt.Fprint(w, result)
}
}

// BillsShapefilesHandler returns a GeoJSON FeatureCollection containing parish
// polygons joined with the bills data. It accepts filtering by year, bill_type,
// count_type, etc.
func (s *Server) BillsShapefilesHandler() http.HandlerFunc {
// Base query with materialized CTE and spatial index hints for performance
baseQuery := `
WITH filtered_bills AS MATERIALIZED (
SELECT
b.parish_id,
b.count_type,
b.count,
b.year
FROM
bom.bill_of_mortality b
WHERE 1=1
-- Dynamic bill filters will be added here
),
parish_data AS (
SELECT
parishes_shp.id,
parishes_shp.par,
parishes_shp.civ_par,
parishes_shp.dbn_par,
parishes_shp.omeka_par,
parishes_shp.subunit,
parishes_shp.city_cnty,
parishes_shp.start_yr,
parishes_shp.sp_total,
parishes_shp.sp_per,
COALESCE(SUM(CASE WHEN fb.count_type = 'Buried' THEN fb.count ELSE 0 END), 0) as total_buried,
COALESCE(SUM(CASE WHEN fb.count_type = 'Plague' THEN fb.count ELSE 0 END), 0) as total_plague,
COUNT(fb.parish_id) as bill_count,
parishes_shp.geom_01
FROM
bom.parishes_shp
LEFT JOIN
filtered_bills fb ON fb.parish_id = parishes_shp.id
WHERE 1=1
-- Dynamic parish filters will be added here
GROUP BY
parishes_shp.id, parishes_shp.par, parishes_shp.civ_par, parishes_shp.dbn_par,
parishes_shp.omeka_par, parishes_shp.subunit, parishes_shp.city_cnty,
parishes_shp.start_yr, parishes_shp.sp_total, parishes_shp.sp_per, parishes_shp.geom_01
)
SELECT json_build_object(
'type', 'FeatureCollection',
'features', COALESCE(json_agg(features.feature), '[]'::json)
)
FROM (
SELECT json_build_object(
'type', 'Feature',
'id', id,
'properties', json_build_object(
'par', par,
'civ_par', civ_par,
'dbn_par', dbn_par,
'omeka_par', omeka_par,
'subunit', subunit,
'city_cnty', city_cnty,
'start_yr', start_yr,
'sp_total', sp_total,
'sp_per', sp_per,
'total_buried', total_buried,
'total_plague', total_plague,
'bill_count', bill_count
),
'geometry', ST_AsGeoJSON(
ST_Transform(
ST_SetSRID(geom_01, 27700),
4326
),
6
)::json
) AS feature
FROM parish_data
) AS features;
`

return func(w http.ResponseWriter, r *http.Request) {
// Parse query parameters
year := r.URL.Query().Get("year")
startYear := r.URL.Query().Get("start-year")
endYear := r.URL.Query().Get("end-year")
subunit := r.URL.Query().Get("subunit")
cityCounty := r.URL.Query().Get("city_cnty")
billType := r.URL.Query().Get("bill-type")
countType := r.URL.Query().Get("count-type")
parish := r.URL.Query().Get("parish")

// Build the query with separate filters for bills and parishes
billFilters, parishFilters := buildSeparateFilters(
year, startYear, endYear, subunit, cityCounty, billType, countType, parish)

// Apply the filters to their respective sections
query := strings.Replace(baseQuery, "-- Dynamic bill filters will be added here", billFilters, 1)
query = strings.Replace(query, "-- Dynamic parish filters will be added here", parishFilters, 1)

// Execute query with a timeout context to prevent long-running queries
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

var result string
err := s.DB.QueryRow(ctx, query).Scan(&result)
if err != nil {
log.Printf("Error executing bills shapefile query: %v", err)
// Check for context deadline exceeded to provide better error messaging
if ctx.Err() == context.DeadlineExceeded {
log.Printf("Query timed out, consider optimizing or using more specific filters")
http.Error(w, "Query timed out. Please try with more specific filters.", http.StatusRequestTimeout)
return
}
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}

// Set appropriate headers for GeoJSON response with optimized caching
w.Header().Set("Content-Type", "application/geo+json")
w.Header().Set("Cache-Control", "public, max-age=86400") // 24 hours cache
w.Header().Set("Vary", "Accept-Encoding") // Allow caching of different encodings
fmt.Fprint(w, result)
}
}

// buildSeparateFilters constructs separate SQL filters for bills and parishes based on URL parameters
func buildSeparateFilters(year, startYear, endYear, subunit, cityCounty, billType, countType, parish string) (string, string) {
var billFilters []string
var parishFilters []string

// Add filters based on provided parameters
if year != "" {
if yearInt, err := strconv.Atoi(year); err == nil {
billFilters = append(billFilters, fmt.Sprintf("AND b.year = %d", yearInt))
parishFilters = append(parishFilters, fmt.Sprintf("AND parishes_shp.start_yr = %d", yearInt))
}
} else {
// Use start-year and end-year if provided
if startYear != "" {
if startYearInt, err := strconv.Atoi(startYear); err == nil {
billFilters = append(billFilters, fmt.Sprintf("AND b.year >= %d", startYearInt))
}
}
if endYear != "" {
if endYearInt, err := strconv.Atoi(endYear); err == nil {
billFilters = append(billFilters, fmt.Sprintf("AND b.year <= %d", endYearInt))
}
}
}

// Parish-specific filters
if subunit != "" {
parishFilters = append(parishFilters, fmt.Sprintf("AND parishes_shp.subunit = '%s'", subunit))
}

if cityCounty != "" {
parishFilters = append(parishFilters, fmt.Sprintf("AND parishes_shp.city_cnty = '%s'", cityCounty))
}

// Bills-specific filters
if billType != "" && isValidBillType(billType) {
billFilters = append(billFilters, fmt.Sprintf("AND b.bill_type = '%s'", billType))
}

if countType != "" && isValidCountType(countType) {
billFilters = append(billFilters, fmt.Sprintf("AND b.count_type = '%s'", countType))
}

// Add parish filter to both queries to ensure they're properly joined
if parish != "" {
parishIDs := strings.Split(parish, ",")
var validParishIDs []string

for _, id := range parishIDs {
if trimmedID := strings.TrimSpace(id); trimmedID != "" {
if _, err := strconv.Atoi(trimmedID); err == nil {
validParishIDs = append(validParishIDs, trimmedID)
}
}
}

if len(validParishIDs) > 0 {
parishFilter := fmt.Sprintf("AND parishes_shp.id IN (%s)", strings.Join(validParishIDs, ","))
parishFilters = append(parishFilters, parishFilter)
billFilter := fmt.Sprintf("AND b.parish_id IN (%s)", strings.Join(validParishIDs, ","))
billFilters = append(billFilters, billFilter)
}
}

return strings.Join(billFilters, " "), strings.Join(parishFilters, " ")
}
20 changes: 19 additions & 1 deletion endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,24 @@ func (s *Server) EndpointsHandler() http.HandlerFunc {
baseurl + "/bom/geometries",
nil,
},
{
"BOM: Bills data with parish polygons",
baseurl + "/bom/bills-geometries",
[]ExampleURL{
{
baseurl + "/bom/bills-geometries?year=1665",
"Bills data with parish polygons for a specific year",
},
{
baseurl + "/bom/bills-geometries?start-year=1664&end-year=1666",
"Bills data with parish polygons for a range of years",
},
{
baseurl + "/bom/bills-geometries?start-year=1664&end-year=1666&bill-type=Weekly&count-type=Buried",
"Bills data with parish polygons filtered by bill type and count type",
},
},
},
{
"BOM: Bills of Mortality",
baseurl + "/bom/bills?start-year=1636&end-year=1754",
Expand Down Expand Up @@ -314,4 +332,4 @@ func (s *Server) EndpointsHandler() http.HandlerFunc {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, resp)
}
}
}
1 change: 1 addition & 0 deletions routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ func (s *Server) Routes() {
s.Router.HandleFunc("/bom/totalbills", s.TotalBillsHandler()).Methods("GET", "HEAD")
s.Router.HandleFunc("/bom/statistics", s.StatisticsHandler()).Methods("GET", "HEAD")
s.Router.HandleFunc("/bom/bills", s.BillsHandler()).Methods("GET", "HEAD")
s.Router.HandleFunc("/bom/bills-geometries", s.BillsShapefilesHandler()).Methods("GET", "HEAD")
s.Router.HandleFunc("/bom/christenings", s.ChristeningsHandler()).Methods("GET", "HEAD")
s.Router.HandleFunc("/bom/causes", s.DeathCausesHandler()).Methods("GET", "HEAD")
s.Router.HandleFunc("/bom/list-deaths", s.ListCausesHandler()).Methods("GET", "HEAD")
Expand Down
Loading