You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

152 lines
3.7 KiB
Go

package citydb
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/doug-martin/goqu/v9"
_ "modernc.org/sqlite"
"prayertimes/pkg/prayer"
)
type Provider struct {
db *goqu.Database
}
func New(db *goqu.Database) Provider {
return Provider{db: db}
}
func Open(path string) (Provider, error) {
conn, err := sql.Open("sqlite", path)
if err != nil {
return Provider{}, fmt.Errorf("failed to open cities database: %w", err)
}
if err := conn.Ping(); err != nil {
return Provider{}, fmt.Errorf("failed to connect to cities database: %w", err)
}
return New(goqu.New("sqlite3", conn)), nil
}
func (p Provider) Close() error {
if err := p.db.Db.(*sql.DB).Close(); err != nil {
return fmt.Errorf("failed to close cities database: %w", err)
}
return nil
}
type locationRow struct {
ID int `db:"geoname_id"`
Name string `db:"name"`
ASCIIName string `db:"ascii_name"`
AlternateNames string `db:"alternate_names"`
CountryCode string `db:"country_code"`
Latitude float64 `db:"latitude"`
Longitude float64 `db:"longitude"`
DistanceSq float64 `db:"distance_sq"`
}
func (p Provider) SearchLocations(ctx context.Context, query string) ([]prayer.Location, error) {
query = strings.TrimSpace(query)
if query == "" {
return []prayer.Location{}, nil
}
pattern := "%" + query + "%"
q := p.db.
From("cities").
Select(
goqu.I("geoname_id"),
goqu.I("name"),
goqu.I("ascii_name"),
goqu.COALESCE(goqu.I("alternate_names"), "").As("alternate_names"),
goqu.I("country_code"),
goqu.I("latitude"),
goqu.I("longitude"),
goqu.V(0).As("distance_sq"),
).
Where(
goqu.Or(
goqu.L("name LIKE ? COLLATE NOCASE", pattern),
goqu.L("ascii_name LIKE ? COLLATE NOCASE", pattern),
goqu.L("alternate_names LIKE ? COLLATE NOCASE", pattern),
),
).
Order(goqu.I("population").Desc(), goqu.I("name").Asc()).
Limit(50)
var rows []locationRow
if err := q.ScanStructsContext(ctx, &rows); err != nil {
return nil, fmt.Errorf("failed to query locations from cities database: %w", err)
}
locations := make([]prayer.Location, 0, len(rows))
for _, row := range rows {
locations = append(locations, prayer.Location{
ID: row.ID,
Name: row.Name,
ASCIIName: row.ASCIIName,
AlternateNames: row.AlternateNames,
CountryCode: row.CountryCode,
Latitude: row.Latitude,
Longitude: row.Longitude,
})
}
return locations, nil
}
func (p Provider) SearchLocationsByCoords(ctx context.Context, coords prayer.Coordinates) ([]prayer.Location, error) {
q := p.db.
From("cities").
Select(
goqu.I("geoname_id"),
goqu.I("name"),
goqu.I("ascii_name"),
goqu.COALESCE(goqu.I("alternate_names"), "").As("alternate_names"),
goqu.I("country_code"),
goqu.I("latitude"),
goqu.I("longitude"),
goqu.L(
"((latitude - ?) * (latitude - ?) + (longitude - ?) * (longitude - ?))",
coords.Latitude,
coords.Latitude,
coords.Longitude,
coords.Longitude,
).As("distance_sq"),
).
Order(
goqu.I("distance_sq").Asc(),
goqu.I("population").Desc(),
).
Limit(50)
var rows []locationRow
if err := q.ScanStructsContext(ctx, &rows); err != nil {
return nil, fmt.Errorf("failed to query locations by coordinates from cities database: %w", err)
}
locations := make([]prayer.Location, 0, len(rows))
for _, row := range rows {
locations = append(locations, prayer.Location{
ID: row.ID,
Name: row.Name,
ASCIIName: row.ASCIIName,
AlternateNames: row.AlternateNames,
CountryCode: row.CountryCode,
Latitude: row.Latitude,
Longitude: row.Longitude,
})
}
return locations, nil
}