diff --git a/cmd/api_server.go b/cmd/api_server.go index ccdef72..4ef8e67 100644 --- a/cmd/api_server.go +++ b/cmd/api_server.go @@ -27,11 +27,11 @@ func ApiServer(zipFile string, port int, debug bool) { } log.Printf("Loading CodePoint data from: %s", zipFile) - spatialIndex, err := spatialindex.NewCodePointSpatialIndex(zipFile) + idx, err := spatialindex.NewCodePointSpatialIndex(zipFile) if err != nil { log.Fatalf("failed to create spatial index: %v", err) } - log.Printf("CodePoint spatial index created with %d entries", spatialIndex.Len()) + log.Printf("CodePoint spatial index created with %d entries", idx.Len()) r := gin.New() @@ -62,8 +62,8 @@ func ApiServer(zipFile string, port int, debug bool) { cache := memoize.NewMemoizer(5*time.Minute, 10*time.Minute) - r.GET("/v1/postcode/codepoints", routes.CodePointSearch(spatialIndex)) - r.GET("/v1/postcode/polygons", routes.PolygonSearch(spatialIndex, cache)) + r.GET("/v1/postcode/codepoints", routes.CodePointSearch(idx)) + r.GET("/v1/postcode/polygons", routes.PolygonSearch(idx, cache)) addr := fmt.Sprintf(":%d", port) log.Printf("Starting HTTP API Server on port %d...", port) diff --git a/routes/search.go b/routes/search.go index 9fabd45..ef01027 100644 --- a/routes/search.go +++ b/routes/search.go @@ -26,7 +26,7 @@ type SearchResponse struct { const MAX_BOUNDS = 5000 // Maximum bounds in meters (5 KM) -func CodePointSearch(spatialIndex *spatialindex.SpatialIndex) func(c *gin.Context) { +func CodePointSearch(idx spatialindex.SpatialIndex) func(c *gin.Context) { return func(c *gin.Context) { bbox, err := parseBBox(c.Query("bbox")) if err != nil { @@ -39,7 +39,7 @@ func CodePointSearch(spatialIndex *spatialindex.SpatialIndex) func(c *gin.Contex return } - results, err := spatialIndex.Search(bbox) + results, err := idx.Search(bbox) if err != nil { log.Printf("error while fetching postcode data: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": "An internal server error occurred"}) @@ -53,7 +53,7 @@ func CodePointSearch(spatialIndex *spatialindex.SpatialIndex) func(c *gin.Contex } } -func PolygonSearch(spatialIndex *spatialindex.SpatialIndex, cache *memoize.Memoizer) func(c *gin.Context) { +func PolygonSearch(idx spatialindex.SpatialIndex, cache *memoize.Memoizer) func(c *gin.Context) { return func(c *gin.Context) { bbox, err := parseBBox(c.Query("bbox")) if err != nil { @@ -71,7 +71,7 @@ func PolygonSearch(spatialIndex *spatialindex.SpatialIndex, cache *memoize.Memoi requested := make(map[string]struct{}, 100) districts := make(map[string]struct{}, 20) - err = spatialIndex.SearchIter(bbox, func(min, max [2]uint32, postcode string) bool { + err = idx.SearchIter(bbox, func(min, max [2]uint32, postcode string) bool { district := strings.Split(postcode, " ")[0] // Take the first part of the postcode districts[district] = struct{}{} if tooBig { diff --git a/spatial-index/codepoint.go b/spatial-index/codepoint.go index d66ee50..b793fd0 100644 --- a/spatial-index/codepoint.go +++ b/spatial-index/codepoint.go @@ -16,12 +16,18 @@ type CodePoint struct { Northing uint32 `json:"northing"` } -type SpatialIndex struct { +type SpatialIndex interface { + Search(bounds []uint32) (*[]CodePoint, error) + SearchIter(bounds []uint32, iter func(min, max [2]uint32, data string) bool) error + Len() int +} + +type RtreeSpatialIndex struct { tree *rtree.RTreeGN[uint32, string] } -func NewCodePointSpatialIndex(zipFile string) (*SpatialIndex, error) { - idx := SpatialIndex{ +func NewCodePointSpatialIndex(zipFile string) (SpatialIndex, error) { + idx := RtreeSpatialIndex{ tree: &rtree.RTreeGN[uint32, string]{}, } @@ -33,7 +39,7 @@ func NewCodePointSpatialIndex(zipFile string) (*SpatialIndex, error) { return &idx, nil } -func (idx *SpatialIndex) Search(bounds []uint32) (*[]CodePoint, error) { +func (idx *RtreeSpatialIndex) Search(bounds []uint32) (*[]CodePoint, error) { results := make([]CodePoint, 0, 100) err := idx.SearchIter(bounds, func(min, max [2]uint32, data string) bool { @@ -52,7 +58,7 @@ func (idx *SpatialIndex) Search(bounds []uint32) (*[]CodePoint, error) { return &results, nil } -func (idx *SpatialIndex) SearchIter(bounds []uint32, iter func(min, max [2]uint32, data string) bool) error { +func (idx *RtreeSpatialIndex) SearchIter(bounds []uint32, iter func(min, max [2]uint32, data string) bool) error { if len(bounds) != 4 { return fmt.Errorf("search bounds must contain exactly 4 values: min_easting, min_northing, max_easting, max_northing") } @@ -64,11 +70,11 @@ func (idx *SpatialIndex) SearchIter(bounds []uint32, iter func(min, max [2]uint3 return nil } -func (idx *SpatialIndex) Len() int { +func (idx *RtreeSpatialIndex) Len() int { return idx.tree.Len() } -func (idx *SpatialIndex) importCodePoint(zipPath string) error { +func (idx *RtreeSpatialIndex) importCodePoint(zipPath string) error { r, err := zip.OpenReader(zipPath) if err != nil { @@ -92,7 +98,7 @@ func (idx *SpatialIndex) importCodePoint(zipPath string) error { return nil } -func (idx *SpatialIndex) processCSV(f *zip.File) error { +func (idx *RtreeSpatialIndex) processCSV(f *zip.File) error { r, err := f.Open() if err != nil { return fmt.Errorf("failed to open embedded file %s in zip: %w", f.Name, err) diff --git a/spatial-index/codepoint_test.go b/spatial-index/codepoint_test.go index 3806651..6cb5f1a 100644 --- a/spatial-index/codepoint_test.go +++ b/spatial-index/codepoint_test.go @@ -66,7 +66,7 @@ func TestSearch(t *testing.T) { } func TestSearchIter_InvalidBounds(t *testing.T) { - idx := &SpatialIndex{tree: &rtree.RTreeGN[uint32, string]{}} + idx := &RtreeSpatialIndex{tree: &rtree.RTreeGN[uint32, string]{}} err := idx.SearchIter([]uint32{1, 2, 3}, func([2]uint32, [2]uint32, string) bool { return true }) require.Error(t, err) require.Contains(t, err.Error(), "bounds must contain exactly 4 values")