Add map section with geocoded family pins
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user