Files
macker/server/main.go
T
2026-10-28 14:04:39 -05:00

171 lines
4.5 KiB
Go

package main
import (
"context"
_ "embed"
"encoding/gob"
"encoding/json"
"flag"
"io"
"log"
"math"
"net"
"net/http"
"os"
"sync"
"time"
"macker/datapoint"
)
//go:embed index.html
var page []byte
type location struct {
Point datapoint.DataPoint `json:"point"`
UpdatedAt time.Time `json:"updated_at"`
ReceivedAt time.Time `json:"received_at"`
Nearby addressResult `json:"nearby"`
}
type tracker struct {
mu sync.RWMutex
latest *location
geocoder *geocoder
addresses *addressIndex
}
func (t *tracker) accept(points []datapoint.DataPoint, now time.Time) {
t.mu.Lock()
defer t.mu.Unlock()
for _, p := range points {
if math.IsNaN(p.Lat) || math.IsInf(p.Lat, 0) || math.IsNaN(p.Long) || math.IsInf(p.Long, 0) || p.Lat < -90 || p.Lat > 90 || p.Long < -180 || p.Long > 180 {
continue
}
if p.Speed != nil && (math.IsNaN(*p.Speed) || math.IsInf(*p.Speed, 0) || *p.Speed < 0) {
p.Speed = nil
}
updated := now
if p.Timestamp != 0 {
if p.Timestamp > uint64(now.Add(time.Minute).UnixMilli()) {
continue
}
updated = time.UnixMilli(int64(p.Timestamp))
}
if t.latest != nil && !updated.After(t.latest.UpdatedAt) {
continue
}
t.latest = &location{Point: p, UpdatedAt: updated, ReceivedAt: now}
}
}
func (t *tracker) receive(c net.Conn) {
defer c.Close()
_ = c.SetReadDeadline(time.Now().Add(10 * time.Second))
var raw []byte
if err := gob.NewDecoder(io.LimitReader(c, datapoint.MaxBatchBytes+1024)).Decode(&raw); err != nil {
log.Printf("decode batch: %v", err)
return
}
points, err := datapoint.UnzipBatch(raw)
if err != nil {
log.Printf("decode points: %v", err)
return
}
t.accept(points, time.Now().UTC())
}
func (t *tracker) handler() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("GET /api/location", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
t.mu.RLock()
if t.latest == nil {
t.mu.RUnlock()
_ = json.NewEncoder(w).Encode(nil)
return
}
snapshot := *t.latest
t.mu.RUnlock()
if t.addresses != nil {
snapshot.Nearby = t.addresses.view(snapshot.Point.Lat, snapshot.Point.Long)
} else {
snapshot.Nearby = t.geocoder.view(snapshot.Point.Lat, snapshot.Point.Long, time.Now())
}
_ = json.NewEncoder(w).Encode(snapshot)
})
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write(page)
})
return mux
}
func main() {
ingest := flag.String("ingest", ":2948", "TCP address for car GPS batches")
web := flag.String("http", ":8080", "HTTP address for the map")
geocodeURL := flag.String("geocoder", os.Getenv("MACKER_GEOCODER_URL"), "Nominatim-compatible reverse geocoding endpoint (empty disables address lookups)")
addressFile := flag.String("addresses", "data/mississippi-addresses.json", "local OSM address index (takes precedence over geocoder)")
importOSM := flag.String("import-osm", "", "import address nodes/ways from OSM XML into -addresses, then exit")
flag.Parse()
if *importOSM != "" {
count, err := buildAddresses(*importOSM, *addressFile)
if err != nil {
log.Fatal(err)
}
log.Printf("Saved %d local addresses to %s", count, *addressFile)
return
}
var local *addressIndex
if *addressFile != "" {
var err error
local, err = loadAddresses(*addressFile)
if err != nil && !os.IsNotExist(err) {
log.Fatal(err)
}
if os.IsNotExist(err) {
log.Printf("Local address data missing: run scripts/prepare-addresses.sh, then restart")
}
if local != nil {
log.Printf("Loaded %d local addresses", local.count)
}
}
var gc *geocoder
var err error
if local == nil {
gc, err = newGeocoder(*geocodeURL, os.Getenv("MACKER_GEOCODER_KEY"))
}
if err != nil {
log.Fatal(err)
}
ln, err := net.Listen("tcp", *ingest)
if err != nil {
log.Fatal(err)
}
defer ln.Close()
t := &tracker{geocoder: gc, addresses: local}
if gc != nil {
go t.runGeocoder(context.Background())
}
go func() {
// Bound concurrent decoders and their memory usage.
slots := make(chan struct{}, 32)
for {
c, err := ln.Accept()
if err != nil {
return
}
select {
case slots <- struct{}{}:
go func() { defer func() { <-slots }(); t.receive(c) }()
default:
c.Close()
}
}
}()
log.Printf("GPS receiver on %s; map on %s", *ingest, *web)
srv := &http.Server{Addr: *web, Handler: t.handler(), ReadHeaderTimeout: 5 * time.Second, WriteTimeout: 15 * time.Second, IdleTimeout: 60 * time.Second}
log.Fatal(srv.ListenAndServe())
}