- Fix Viz3D exports to include flow visualization functions - Export setFlowLayerVisible, setDwellLayerVisible, setCorridorLayerVisible - Export setFlowTimeFilter, setFlowData, setDwellData, setCorridorData - Remove duplicate setDwellLayerVisible function definition This completes the crowd flow visualization feature that was already implemented in the backend (flow.go) and frontend (crowdflow.js, viz3d.js) but had missing exports in the Viz3D module.
453 lines
14 KiB
Go
453 lines
14 KiB
Go
// Package api provides REST API handlers for self-improving localization.
|
|
package api
|
|
|
|
import (
|
|
"log"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/spaxel/mothership/internal/localization"
|
|
)
|
|
|
|
// LocalizationHandler manages self-improving localization API endpoints.
|
|
type LocalizationHandler struct {
|
|
groundTruthStore *localization.GroundTruthStore
|
|
spatialWeightLearner *localization.SpatialWeightLearner
|
|
weightLearner *localization.WeightLearner
|
|
weightStore *localization.WeightStore
|
|
selfImprovingLocalizer *localization.SelfImprovingLocalizer
|
|
}
|
|
|
|
// NewLocalizationHandler creates a new localization API handler.
|
|
func NewLocalizationHandler(
|
|
gtStore *localization.GroundTruthStore,
|
|
swLearner *localization.SpatialWeightLearner,
|
|
wLearner *localization.WeightLearner,
|
|
wStore *localization.WeightStore,
|
|
sil *localization.SelfImprovingLocalizer,
|
|
) *LocalizationHandler {
|
|
return &LocalizationHandler{
|
|
groundTruthStore: gtStore,
|
|
spatialWeightLearner: swLearner,
|
|
weightLearner: wLearner,
|
|
weightStore: wStore,
|
|
selfImprovingLocalizer: sil,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes registers localization endpoints.
|
|
func (h *LocalizationHandler) RegisterRoutes(r chi.Router) {
|
|
// Learned weights endpoints
|
|
r.Get("/api/localization/weights", h.getWeights)
|
|
r.Get("/api/localization/weights/{linkID}", h.getLinkWeight)
|
|
r.Post("/api/localization/weights/reset", h.resetWeights)
|
|
r.Get("/api/localization/weights/stats", h.getWeightStats)
|
|
|
|
// Spatial weights endpoints
|
|
r.Get("/api/localization/spatial-weights", h.getSpatialWeights)
|
|
r.Get("/api/localization/spatial-weights/stats", h.getSpatialWeightStats)
|
|
r.Get("/api/localization/spatial-weights/zone/{zoneX}/{zoneY}", h.getSpatialWeightsForZone)
|
|
|
|
// Ground truth endpoints
|
|
r.Get("/api/localization/groundtruth/samples", h.getGroundTruthSamples)
|
|
r.Get("/api/localization/groundtruth/stats", h.getGroundTruthStats)
|
|
r.Post("/api/localization/groundtruth/compute-accuracy", h.computeWeeklyAccuracy)
|
|
|
|
// Accuracy and improvement endpoints
|
|
r.Get("/api/localization/accuracy/history", h.getAccuracyHistory)
|
|
r.Get("/api/localization/accuracy/current", h.getCurrentAccuracy)
|
|
r.Get("/api/localization/accuracy/improvement", h.getImprovementStats)
|
|
|
|
// Learning progress endpoints
|
|
r.Get("/api/localization/learning/progress", h.getLearningProgress)
|
|
r.Get("/api/localization/learning/history", h.getImprovementHistory)
|
|
|
|
// Self-improving localizer endpoints
|
|
r.Get("/api/localization/self-improving/status", h.getSelfImprovingStatus)
|
|
r.Post("/api/localization/self-improving/process", h.processLearning)
|
|
}
|
|
|
|
// getWeights handles GET /api/localization/weights
|
|
func (h *LocalizationHandler) getWeights(w http.ResponseWriter, r *http.Request) {
|
|
if h.weightLearner == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "weight learner not available")
|
|
return
|
|
}
|
|
|
|
weights := h.weightLearner.GetLearnedWeights().GetAllWeights()
|
|
stats := h.weightLearner.GetAllStats()
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"weights": weights,
|
|
"stats": stats,
|
|
})
|
|
}
|
|
|
|
// getLinkWeight handles GET /api/localization/weights/{linkID}
|
|
func (h *LocalizationHandler) getLinkWeight(w http.ResponseWriter, r *http.Request) {
|
|
if h.weightLearner == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "weight learner not available")
|
|
return
|
|
}
|
|
|
|
linkID := chi.URLParam(r, "linkID")
|
|
weights := h.weightLearner.GetLearnedWeights()
|
|
stats := h.weightLearner.GetLinkStats(linkID)
|
|
|
|
result := map[string]interface{}{
|
|
"link_id": linkID,
|
|
"weight": weights.GetLinkWeight(linkID),
|
|
"sigma": weights.GetLinkSigma(linkID),
|
|
}
|
|
|
|
if stats != nil {
|
|
result["stats"] = stats
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
// resetWeights handles POST /api/localization/weights/reset
|
|
func (h *LocalizationHandler) resetWeights(w http.ResponseWriter, r *http.Request) {
|
|
if h.weightLearner == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "weight learner not available")
|
|
return
|
|
}
|
|
|
|
// Reset all weights to default by creating a fresh LearnedWeights
|
|
weights := localization.NewLearnedWeights()
|
|
|
|
// Persist reset
|
|
if h.weightStore != nil {
|
|
if err := h.weightStore.SaveWeights(weights); err != nil {
|
|
log.Printf("[WARN] Failed to save reset weights: %v", err)
|
|
}
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{"status": "weights_reset"})
|
|
}
|
|
|
|
// getWeightStats handles GET /api/localization/weights/stats
|
|
func (h *LocalizationHandler) getWeightStats(w http.ResponseWriter, r *http.Request) {
|
|
if h.weightLearner == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "weight learner not available")
|
|
return
|
|
}
|
|
|
|
stats := h.weightLearner.GetAllStats()
|
|
progress := h.weightLearner.GetLearningProgress()
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"stats": stats,
|
|
"progress": progress,
|
|
})
|
|
}
|
|
|
|
// getSpatialWeights handles GET /api/localization/spatial-weights
|
|
func (h *LocalizationHandler) getSpatialWeights(w http.ResponseWriter, r *http.Request) {
|
|
if h.spatialWeightLearner == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "spatial weight learner not available")
|
|
return
|
|
}
|
|
|
|
weights := h.spatialWeightLearner.GetAllWeights()
|
|
stats := h.spatialWeightLearner.GetWeightStats()
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"spatial_weights": weights,
|
|
"stats": stats,
|
|
})
|
|
}
|
|
|
|
// getSpatialWeightStats handles GET /api/localization/spatial-weights/stats
|
|
func (h *LocalizationHandler) getSpatialWeightStats(w http.ResponseWriter, r *http.Request) {
|
|
if h.spatialWeightLearner == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "spatial weight learner not available")
|
|
return
|
|
}
|
|
|
|
stats := h.spatialWeightLearner.GetWeightStats()
|
|
|
|
writeJSON(w, http.StatusOK, stats)
|
|
}
|
|
|
|
// getSpatialWeightsForZone handles GET /api/localization/spatial-weights/zone/{zoneX}/{zoneY}
|
|
func (h *LocalizationHandler) getSpatialWeightsForZone(w http.ResponseWriter, r *http.Request) {
|
|
if h.spatialWeightLearner == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "spatial weight learner not available")
|
|
return
|
|
}
|
|
|
|
zoneX, err := strconv.Atoi(chi.URLParam(r, "zoneX"))
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "invalid zoneX")
|
|
return
|
|
}
|
|
|
|
zoneY, err := strconv.Atoi(chi.URLParam(r, "zoneY"))
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusBadRequest, "invalid zoneY")
|
|
return
|
|
}
|
|
|
|
weights := h.spatialWeightLearner.GetWeightsForZone(zoneX, zoneY)
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"zone_x": zoneX,
|
|
"zone_y": zoneY,
|
|
"weights": weights,
|
|
})
|
|
}
|
|
|
|
// getGroundTruthSamples handles GET /api/localization/groundtruth/samples
|
|
func (h *LocalizationHandler) getGroundTruthSamples(w http.ResponseWriter, r *http.Request) {
|
|
if h.groundTruthStore == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "ground truth store not available")
|
|
return
|
|
}
|
|
|
|
// Parse query parameters
|
|
personID := r.URL.Query().Get("person")
|
|
limitStr := r.URL.Query().Get("limit")
|
|
limit := 100
|
|
if limitStr != "" {
|
|
if n, err := strconv.Atoi(limitStr); err == nil && n > 0 {
|
|
limit = n
|
|
}
|
|
}
|
|
|
|
var samples []localization.GroundTruthSample
|
|
var err error
|
|
|
|
if personID != "" {
|
|
// Get samples for specific person
|
|
samples, err = h.groundTruthStore.GetSamplesInTimeRange(
|
|
time.Now().Add(-24*time.Hour), time.Now())
|
|
// Filter by person
|
|
filtered := make([]localization.GroundTruthSample, 0)
|
|
for _, s := range samples {
|
|
if s.PersonID == personID {
|
|
filtered = append(filtered, s)
|
|
}
|
|
}
|
|
samples = filtered
|
|
} else {
|
|
samples, err = h.groundTruthStore.GetRecentSamples(limit)
|
|
}
|
|
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"samples": samples,
|
|
"count": len(samples),
|
|
})
|
|
}
|
|
|
|
// getGroundTruthStats handles GET /api/localization/groundtruth/stats
|
|
func (h *LocalizationHandler) getGroundTruthStats(w http.ResponseWriter, r *http.Request) {
|
|
if h.groundTruthStore == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "ground truth store not available")
|
|
return
|
|
}
|
|
|
|
total, err := h.groundTruthStore.GetTotalSampleCount()
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
byPerson, err := h.groundTruthStore.GetSampleCountByPerson()
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
today, err := h.groundTruthStore.GetSamplesTodayCount()
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
zoneCounts, err := h.groundTruthStore.GetZoneSampleCounts()
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"total_samples": total,
|
|
"today_samples": today,
|
|
"by_person": byPerson,
|
|
"zone_counts": zoneCounts,
|
|
})
|
|
}
|
|
|
|
// computeWeeklyAccuracy handles POST /api/localization/groundtruth/compute-accuracy
|
|
func (h *LocalizationHandler) computeWeeklyAccuracy(w http.ResponseWriter, r *http.Request) {
|
|
if h.groundTruthStore == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "ground truth store not available")
|
|
return
|
|
}
|
|
|
|
// Get current week
|
|
week := localization.GetWeekString(time.Now())
|
|
|
|
if err := h.groundTruthStore.ComputeWeeklyAccuracy(week); err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"status": "accuracy_computed",
|
|
"week": week,
|
|
})
|
|
}
|
|
|
|
// getAccuracyHistory handles GET /api/localization/accuracy/history
|
|
func (h *LocalizationHandler) getAccuracyHistory(w http.ResponseWriter, r *http.Request) {
|
|
if h.groundTruthStore == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "ground truth store not available")
|
|
return
|
|
}
|
|
|
|
weeksStr := r.URL.Query().Get("weeks")
|
|
weeks := 12 // Default 12 weeks
|
|
if weeksStr != "" {
|
|
if n, err := strconv.Atoi(weeksStr); err == nil && n > 0 {
|
|
weeks = n
|
|
}
|
|
}
|
|
|
|
records, err := h.groundTruthStore.GetPositionAccuracyHistory(weeks)
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"records": records,
|
|
"weeks": weeks,
|
|
})
|
|
}
|
|
|
|
// getCurrentAccuracy handles GET /api/localization/accuracy/current
|
|
func (h *LocalizationHandler) getCurrentAccuracy(w http.ResponseWriter, r *http.Request) {
|
|
if h.groundTruthStore == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "ground truth store not available")
|
|
return
|
|
}
|
|
|
|
current, err := h.groundTruthStore.GetCurrentPositionAccuracy()
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
if current == nil {
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"message": "no accuracy data for current week",
|
|
})
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, current)
|
|
}
|
|
|
|
// getImprovementStats handles GET /api/localization/accuracy/improvement
|
|
func (h *LocalizationHandler) getImprovementStats(w http.ResponseWriter, r *http.Request) {
|
|
if h.groundTruthStore == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "ground truth store not available")
|
|
return
|
|
}
|
|
|
|
stats, err := h.groundTruthStore.GetPositionImprovementStats()
|
|
if err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, stats)
|
|
}
|
|
|
|
// getLearningProgress handles GET /api/localization/learning/progress
|
|
func (h *LocalizationHandler) getLearningProgress(w http.ResponseWriter, r *http.Request) {
|
|
if h.weightLearner == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "weight learner not available")
|
|
return
|
|
}
|
|
|
|
progress := h.weightLearner.GetLearningProgress()
|
|
stats := h.weightLearner.GetAllStats()
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"progress": progress,
|
|
"stats": stats,
|
|
})
|
|
}
|
|
|
|
// getImprovementHistory handles GET /api/localization/learning/history
|
|
func (h *LocalizationHandler) getImprovementHistory(w http.ResponseWriter, r *http.Request) {
|
|
if h.weightLearner == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "weight learner not available")
|
|
return
|
|
}
|
|
|
|
history := h.weightLearner.GetImprovementHistory()
|
|
stats := h.weightLearner.GetImprovementStats()
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"history": history,
|
|
"stats": stats,
|
|
})
|
|
}
|
|
|
|
// getSelfImprovingStatus handles GET /api/localization/self-improving/status
|
|
func (h *LocalizationHandler) getSelfImprovingStatus(w http.ResponseWriter, r *http.Request) {
|
|
if h.selfImprovingLocalizer == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "self-improving localizer not available")
|
|
return
|
|
}
|
|
|
|
progress := h.selfImprovingLocalizer.GetLearningProgress()
|
|
weights := h.selfImprovingLocalizer.GetLearnedWeights()
|
|
improvementStats := h.selfImprovingLocalizer.GetImprovementStats()
|
|
improvementHistory := h.selfImprovingLocalizer.GetImprovementHistory()
|
|
var bleObsCount int
|
|
if provider, ok := h.selfImprovingLocalizer.GetGroundTruthProvider().(*localization.BLEGroundTruthProvider); ok {
|
|
bleObsCount = provider.GetObservationCount()
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
|
"learning_progress": progress,
|
|
"learned_weights": weights,
|
|
"improvement_stats": improvementStats,
|
|
"improvement_history": improvementHistory,
|
|
"ble_observations_count": bleObsCount,
|
|
})
|
|
}
|
|
|
|
// processLearning handles POST /api/localization/self-improving/process
|
|
func (h *LocalizationHandler) processLearning(w http.ResponseWriter, r *http.Request) {
|
|
if h.weightLearner == nil {
|
|
writeJSONError(w, http.StatusServiceUnavailable, "weight learner not available")
|
|
return
|
|
}
|
|
|
|
if err := h.weightLearner.ProcessLearning(); err != nil {
|
|
writeJSONError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
|
|
// Record error snapshot for improvement tracking
|
|
h.weightLearner.RecordErrorSnapshot()
|
|
|
|
writeJSON(w, http.StatusOK, map[string]string{
|
|
"status": "learning_processed",
|
|
"timestamp": time.Now().Format(time.RFC3339),
|
|
})
|
|
}
|