Add map section with geocoded family pins

This commit is contained in:
Ian Gulliver
2026-08-15 23:21:03 -07:00
parent c56310d219
commit fd52c948db
11 changed files with 350 additions and 13 deletions
+50 -5
View File
@@ -6,18 +6,20 @@ import (
"time"
"heliosian/internal/data"
"heliosian/internal/geocode"
)
const refreshInterval = 5 * time.Minute
type Cache struct {
source data.Source
mu sync.RWMutex
model *Model
source data.Source
geocoder *geocode.Client
mu sync.RWMutex
model *Model
}
func NewCache(source data.Source) (*Cache, error) {
c := &Cache{source: source}
func NewCache(source data.Source, geocoder *geocode.Client) (*Cache, error) {
c := &Cache{source: source, geocoder: geocoder}
if err := c.refresh(); err != nil {
return nil, err
}
@@ -45,6 +47,7 @@ func (c *Cache) refresh() error {
if err != nil {
return err
}
c.geocodeFamilies(model)
c.mu.Lock()
c.model = model
c.mu.Unlock()
@@ -53,3 +56,45 @@ func (c *Cache) refresh() error {
time.Since(start).Round(time.Millisecond))
return nil
}
func (c *Cache) geocodeFamilies(model *Model) {
start := time.Now()
type job struct {
key string
address string
}
jobs := make(chan job)
var wg sync.WaitGroup
var mu sync.Mutex
located := 0
for range 8 {
wg.Add(1)
go func() {
defer wg.Done()
for j := range jobs {
point, err := c.geocoder.Lookup(j.address)
if err != nil {
log.Printf("[ERROR] %v", err)
continue
}
mu.Lock()
family := model.Families[j.key]
family.Lat = point.Lat
family.Lng = point.Lng
model.Families[j.key] = family
located++
mu.Unlock()
}
}()
}
total := 0
for key, family := range model.Families {
if family.Address != "" {
total++
jobs <- job{key: key, address: family.Address}
}
}
close(jobs)
wg.Wait()
log.Printf("geocoded %d of %d family addresses in %s", located, total, time.Since(start).Round(time.Millisecond))
}
+5 -3
View File
@@ -23,11 +23,12 @@ var legacy = map[string]string{
}
type app struct {
cache *Cache
cache *Cache
mapsKey string
}
func Register(mux *http.ServeMux, cache *Cache) {
a := app{cache: cache}
func Register(mux *http.ServeMux, cache *Cache, mapsKey string) {
a := app{cache: cache, mapsKey: mapsKey}
for _, section := range sections {
mux.HandleFunc("GET /"+section, a.page)
}
@@ -60,6 +61,7 @@ func (a app) page(w http.ResponseWriter, r *http.Request) {
"UserName": name,
"UserInitial": strings.ToUpper(name[:1]),
"UserEmail": auth.Email(r),
"MapsKey": a.mapsKey,
}
if err := t.Execute(w, data); err != nil {
log.Printf("[ERROR] render directory page: %v", err)
+2
View File
@@ -28,6 +28,8 @@ type Family struct {
Key string `json:"key"`
Name string `json:"name,omitempty"`
Address string `json:"address,omitempty"`
Lat float64 `json:"lat,omitempty"`
Lng float64 `json:"lng,omitempty"`
PhotoURL string `json:"photoUrl,omitempty"`
PhotoCaption string `json:"photoCaption,omitempty"`
PronunciationURL string `json:"pronunciationUrl,omitempty"`
+62
View File
@@ -0,0 +1,62 @@
// Package geocode resolves street addresses to coordinates via the google geocoding api.
package geocode
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"sync"
)
type Point struct {
Lat float64
Lng float64
}
type Client struct {
key string
mu sync.Mutex
cache map[string]Point
}
func New(key string) *Client {
return &Client{key: key, cache: map[string]Point{}}
}
func (c *Client) Lookup(address string) (Point, error) {
c.mu.Lock()
point, ok := c.cache[address]
c.mu.Unlock()
if ok {
return point, nil
}
resp, err := http.Get("https://maps.googleapis.com/maps/api/geocode/json?address=" +
url.QueryEscape(address) + "&key=" + url.QueryEscape(c.key))
if err != nil {
return Point{}, err
}
defer resp.Body.Close()
var parsed struct {
Status string `json:"status"`
Results []struct {
Geometry struct {
Location struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
} `json:"location"`
} `json:"geometry"`
} `json:"results"`
}
if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil {
return Point{}, err
}
if parsed.Status != "OK" || len(parsed.Results) == 0 {
return Point{}, fmt.Errorf("geocode %q: %s", address, parsed.Status)
}
point = Point{Lat: parsed.Results[0].Geometry.Location.Lat, Lng: parsed.Results[0].Geometry.Location.Lng}
c.mu.Lock()
c.cache[address] = point
c.mu.Unlock()
return point, nil
}