turash/bugulma/backend/internal/service/public_transport_service.go
2025-12-15 10:06:41 +01:00

163 lines
3.9 KiB
Go

package service
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
// PublicTransportService provides read-only access to precomputed public transport data
type PublicTransportService struct {
metadata map[string]interface{}
stops map[string]interface{}
raw map[string]interface{}
baseDir string
}
// NewPublicTransportService attempts to load enriched public transport JSON from data directory
func NewPublicTransportService(baseDir string) (*PublicTransportService, error) {
svc := &PublicTransportService{baseDir: baseDir}
// Try to read enriched JSON first
candidates := []string{
filepath.Join(baseDir, "bugulma_public_transport_enriched.json"),
filepath.Join(baseDir, "bugulma_public_transport.json"),
}
var found bool
for _, p := range candidates {
if _, err := os.Stat(p); err == nil {
b, err := ioutil.ReadFile(p)
if err != nil {
return nil, fmt.Errorf("failed to read public transport file %s: %w", p, err)
}
var raw map[string]interface{}
if err := json.Unmarshal(b, &raw); err != nil {
return nil, fmt.Errorf("failed to unmarshal public transport json %s: %w", p, err)
}
svc.raw = raw
// Extract metadata and common stops if present
if m, ok := raw["metadata"].(map[string]interface{}); ok {
svc.metadata = m
}
if cs, ok := raw["common_stops_directory"].(map[string]interface{}); ok {
svc.stops = cs
} else {
svc.stops = map[string]interface{}{}
}
found = true
break
}
}
if !found {
// No JSON file found, return empty service with baseDir set so GTFS files can still be served
svc.raw = map[string]interface{}{}
svc.metadata = map[string]interface{}{}
svc.stops = map[string]interface{}{}
}
return svc, nil
}
// GetMetadata returns metadata extracted from the data file
func (s *PublicTransportService) GetMetadata() map[string]interface{} {
return s.metadata
}
// ListStops returns a map of stop key -> stop object
func (s *PublicTransportService) ListStops() map[string]interface{} {
return s.stops
}
// GetStop returns a stop by id (key in the common_stops_directory)
func (s *PublicTransportService) GetStop(id string) (interface{}, bool) {
st, ok := s.stops[id]
return st, ok
}
// Search stops by substring match in names (case-insensitive)
func (s *PublicTransportService) SearchStops(query string) map[string]interface{} {
out := map[string]interface{}{}
if query == "" {
return out
}
for k, v := range s.stops {
if k == query {
out[k] = v
continue
}
// try looking into name fields if present
if m, ok := v.(map[string]interface{}); ok {
if name, ok := m["name"].(string); ok {
if containsIgnoreCase(name, query) {
out[k] = v
continue
}
}
if nameEn, ok := m["name_en"].(string); ok {
if containsIgnoreCase(nameEn, query) {
out[k] = v
continue
}
}
}
}
return out
}
// ReadGTFSFile returns file contents from the GTFS export folder if present
func (s *PublicTransportService) ReadGTFSFile(filename string) (string, error) {
p := filepath.Join(s.baseDir, "bugulma_gtfs_export", filename)
b, err := ioutil.ReadFile(p)
if err != nil {
return "", err
}
return string(b), nil
}
// Helper: case-insensitive substring
func containsIgnoreCase(src, sub string) bool {
return len(src) >= len(sub) && (stringIndexIgnoreCase(src, sub) >= 0)
}
func stringIndexIgnoreCase(s, substr string) int {
// naive implementation that converts to lower-case
sLower := []rune{}
subLower := []rune{}
for _, r := range s {
if 'A' <= r && r <= 'Z' {
r = r - 'A' + 'a'
}
sLower = append(sLower, r)
}
for _, r := range substr {
if 'A' <= r && r <= 'Z' {
r = r - 'A' + 'a'
}
subLower = append(subLower, r)
}
sStr := string(sLower)
subStr := string(subLower)
return indexOf(sStr, subStr)
}
func indexOf(s, substr string) int {
if substr == "" {
return 0
}
for i := 0; i+len(substr) <= len(s); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}