Skip to content
64 changes: 64 additions & 0 deletions db/migrations/00009_add_netex_data.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE NETEX_StopPlace (
id VARCHAR(255) PRIMARY KEY,
modification VARCHAR(50),
created_timestamp TIMESTAMPTZ,
changed_timestamp TIMESTAMPTZ,
valid_from_date TIMESTAMPTZ,
valid_to_date TIMESTAMPTZ,
name VARCHAR(255),
name_lang VARCHAR(10),
longitude DECIMAL(9,6),
latitude DECIMAL(8,6),
transport_mode VARCHAR(50),
other_transport_modes TEXT,
stop_place_type VARCHAR(50),
weighting VARCHAR(50),
UNIQUE (id)
);

CREATE TABLE NETEX_Quay (
id VARCHAR(255) PRIMARY KEY,
changed_timestamp TIMESTAMPTZ,
name VARCHAR(255),
name_lang VARCHAR(10),
longitude DECIMAL(9,6),
latitude DECIMAL(8,6),
postal_region VARCHAR(50),
site_ref_stopplace_id VARCHAR(255) REFERENCES NETEX_StopPlace(id) ON DELETE SET NULL,
transport_mode VARCHAR(50),
UNIQUE (id)
);

CREATE TABLE NETEX_StopPlace_QuayRef (
stop_place_id VARCHAR(255) REFERENCES NETEX_StopPlace(id) ON DELETE CASCADE,
quay_id VARCHAR(255) REFERENCES NETEX_Quay(id) ON DELETE CASCADE,
quay_ref_version VARCHAR(50),
PRIMARY KEY (stop_place_id, quay_id),
UNIQUE (stop_place_id, quay_id)
);

CREATE TABLE NETEX_Line (
id TEXT PRIMARY KEY, -- ex: "NAOLIBORG:Line:3B:LOC"
version TEXT,
name TEXT,
short_name TEXT,
transport_mode TEXT,
public_code TEXT,
private_code TEXT,
colour TEXT,
text_colour TEXT,
route_sort_order INTEGER,
UNIQUE (id)
);

-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP TABLE NETEX_StopPlace;
DROP TABLE NETEX_Quay;
DROP TABLE NETEX_StopPlace_QuayRef;
DROP TABLE NETEX_Line;
-- +goose StatementEnd
173 changes: 173 additions & 0 deletions handlers/naolib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package handlers

import (
"database/sql"
"os"

"github.com/gofiber/fiber/v2"
"github.com/plugimt/transat-backend/models"
"github.com/plugimt/transat-backend/services"
"github.com/plugimt/transat-backend/services/naolib/netex"
"github.com/plugimt/transat-backend/services/naolib/siri"
)

const (
ChantrerieStopPlaceId = "FR_NAOLIB:StopPlace:244"
)

type NaolibHandler struct {
service *services.NaolibService
netexService *netex.NetexService
db *sql.DB
}

func NewNaolibHandler(service *services.NaolibService, netexService *netex.NetexService, db *sql.DB) *NaolibHandler {
return &NaolibHandler{
service: service,
netexService: netexService,
db: db,
}
}

func (h *NaolibHandler) GetNextDeparturesChantrerie(c *fiber.Ctx) error {
departures, err := h.service.GetDepartures(ChantrerieStopPlaceId)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}

return c.JSON(departures)
}

func (h *NaolibHandler) ImportNetexOffer(c *fiber.Ctx) error {
var body struct {
Url string `json:"url"`
}

if err := c.BodyParser(&body); err != nil {
return c.Status(fiber.StatusBadRequest).SendString(err.Error())
}

url := body.Url

if url == "" {
return c.Status(fiber.StatusBadRequest).SendString("URL is required")
}

fileName, err := h.netexService.DownloadAndExtractIfNeededOffer(url)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
defer os.Remove(fileName)

netexData, err := h.netexService.DecodeNetexOfferData(fileName)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}

err = h.netexService.SaveNetexOfferToDatabase(netexData)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}

return c.JSON(map[string]any{
"success": true,
})
}

func (h *NaolibHandler) ImportNetexStops(c *fiber.Ctx) error {
var body struct {
Url string `json:"url"`
}

if err := c.BodyParser(&body); err != nil {
return c.Status(fiber.StatusBadRequest).SendString(err.Error())
}

url := body.Url

if url == "" {
return c.Status(fiber.StatusBadRequest).SendString("URL is required")
}

fileName, err := h.netexService.DownloadAndExtractIfNeeded(url)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
defer os.Remove(fileName)

netexData, err := h.netexService.DecodeNetexStopsData(fileName)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}

err = h.netexService.SaveNetexStopsToDatabase(netexData)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}

return c.JSON(map[string]any{
"success": true,
})
}

func (h *NaolibHandler) SearchStopPlace(c *fiber.Ctx) error {
query := c.Query("query")
if query == "" {
return c.Status(fiber.StatusBadRequest).SendString("Query is required")
}

rows, err := h.db.Query("SELECT id, name FROM NETEX_StopPlace WHERE name ILIKE $1", "%"+query+"%")
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
defer rows.Close()

var stopPlaces []models.StopPlace
for rows.Next() {
var stopPlace models.StopPlace
err = rows.Scan(&stopPlace.ID, &stopPlace.Name)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}
stopPlaces = append(stopPlaces, stopPlace)
}

type StopPlaceResponse struct {
ID string `json:"id"`
Name string `json:"name"`
}

stopPlacesArray := make([]StopPlaceResponse, len(stopPlaces))
for i, stopPlace := range stopPlaces {
stopPlacesArray[i] = StopPlaceResponse{
ID: stopPlace.ID,
Name: stopPlace.Name,
}
}

return c.JSON(stopPlacesArray)
}

func (h *NaolibHandler) GenerateNetexRequest(c *fiber.Ctx) error {
stops := []string{"CTRE2", "CTRE4"}
request, err := siri.GenerateStopMonitoringRequest(stops)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error() + "\n")
}

return c.SendString(request + "\n")
}

func (h *NaolibHandler) GetDepartures(c *fiber.Ctx) error {
stopPlaceId := c.Query("stopPlaceId")
if stopPlaceId == "" {
return c.Status(fiber.StatusBadRequest).SendString("Stop place ID is required")
}

departuresMap, err := h.service.GetDepartures(stopPlaceId)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
}

return c.JSON(departuresMap)
}
6 changes: 6 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/plugimt/transat-backend/routes"
"github.com/plugimt/transat-backend/scheduler" // Import our scheduler package
"github.com/plugimt/transat-backend/services"
"github.com/plugimt/transat-backend/services/naolib/netex"
"github.com/plugimt/transat-backend/utils"
"github.com/robfig/cron/v3"

Expand Down Expand Up @@ -119,6 +120,10 @@ func main() {
// Initialize Handlers that need explicit instantiation (e.g., for Cron)
restHandler := restaurantHandler.NewRestaurantHandler(db, translationService, notificationService)

// Naolib
netexService := netex.NewNetexService(db)
naolibService := services.NewNaolibService(db, netexService)

// Initialize Weather Service and Handler
weatherService, err := services.NewWeatherService()
if err != nil {
Expand Down Expand Up @@ -197,6 +202,7 @@ func main() {
routes.SetupWashingMachineRoutes(api) // Setup washing machine routes
routes.SetupWeatherRoutes(api, weatherHandler) // Setup weather routes
routes.SetupNotificationRoutes(api, db, notificationService) // Setup notification test routes
routes.SetupNaolibRoutes(api, naolibService, netexService, db)

app.Get("/health", func(c *fiber.Ctx) error {
return c.SendString("OK")
Expand Down
32 changes: 32 additions & 0 deletions models/naolib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package models

import "time"

type DepartureDirection struct {
Direction string `json:"direction"`
Departures []Departure `json:"departures"`
}

type Departures struct {
DepartureDirectionAller DepartureDirection `json:"aller"`
DepartureDirectionRetour DepartureDirection `json:"retour"`
}

type Departure struct {
Line Line `json:"line"`
LineRef string `json:"lineRef"`
Direction string `json:"direction"`
DestinationName string `json:"destinationName"`
DepartureTime time.Time `json:"departureTime"`
ArrivalTime time.Time `json:"arrivalTime"`
VehicleMode string `json:"vehicleMode"`
}

type Line struct {
ID string `json:"id"`
Name string `json:"name"`
TransportMode string `json:"transportMode"`
Number string `json:"number"`
BackgroundColour string `json:"backgroundColour"`
ForegroundColour string `json:"foregroundColour"`
}
Loading