package main import ( "database/sql" "fmt" "log" "net/http" "os" "strings" "github.com/gin-gonic/gin" _ "github.com/lib/pq" // Driver PostgreSQL "github.com/spf13/viper" ) // Config struct to hold all configuration for our application type Config struct { Database struct { User string `mapstructure:"user"` Password string `mapstructure:"password"` Dbname string `mapstructure:"dbname"` Host string `mapstructure:"host"` Port int `mapstructure:"port"` Sslmode string `mapstructure:"sslmode"` } `mapstructure:"database"` Server struct { Address string `mapstructure:"address"` } `mapstructure:"server"` } // GpsCoord représente les coordonnées GPS reçues en entrée. // Les noms de champs commencent par une majuscule pour être exportables // et donc accessibles par le décodeur JSON de Gin. type GpsCoord struct { Lat float64 `json:"lat" binding:"required"` Lon float64 `json:"lon" binding:"required"` SrcProj int `json:"srcProj" binding:"required"` DstProj int `json:"dstProj" binding:"required"` } // GpsCoordWithZ représente les coordonnées GPS et l'altitude reçues pour le calcul de différence. type GpsCoordWithZ struct { Lat float64 `json:"lat" binding:"required"` Lon float64 `json:"lon" binding:"required"` Z float64 `json:"z" binding:"required"` SrcProj int `json:"srcProj" binding:"required"` DstProj int `json:"dstProj" binding:"required"` } // AltitudeResponse représente la réponse JSON retournée par l'API. type AltitudeResponse struct { Z float64 `json:"z"` } // CorrectedAltitudeResponse représente la réponse JSON pour l'altitude corrigée. type CorrectedAltitudeResponse struct { CorrectedZ float64 `json:"corrected_z"` } // queryAltitude interroge la base de données pour obtenir l'altitude réelle pour des coordonnées données. // C'est une fonction utilitaire pour éviter la duplication de code. func queryOrthometricCorrection(db *sql.DB, lat, lon float64, srcProj, dstProj int) (float64, error) { var altitude float64 query := ` SELECT ST_Value(rast, ST_Transform(ST_SetSRID(ST_MakePoint($2, $1), $3::integer), $4::integer)) AS pixel_value FROM raf20lamber93 WHERE ST_Intersects(rast, ST_Transform(ST_SetSRID(ST_MakePoint($2, $1), $3::integer), $4::integer)); ` // On exécute la requête. QueryRow est idéal car nous attendons au plus une ligne. err := db.QueryRow(query, lat, lon, srcProj, dstProj).Scan(&altitude) if err != nil { // Si aucune ligne n'est trouvée, ST_Value renvoie NULL, ce qui cause une erreur au Scan. // On retourne l'erreur pour qu'elle soit gérée par la fonction appelante. return 0, err } return altitude, nil } // getRealZ est le handler pour notre route. Il prend une connexion à la BDD en paramètre. func getOrthometricCorrection(db *sql.DB) gin.HandlerFunc { return func(c *gin.Context) { var input GpsCoord // On valide et on lie le JSON d'entrée à notre struct GpsCoord. if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Paramètres invalides: " + err.Error()}) return } // On appelle notre fonction utilitaire pour obtenir l'altitude. altitude, err := queryOrthometricCorrection(db, input.Lat, input.Lon, input.SrcProj, input.DstProj) if err != nil { // Si aucune ligne n'est trouvée, ST_Value renvoie NULL, ce qui cause une erreur au Scan. // On peut considérer cette erreur comme un "non trouvé". // Pour une gestion plus fine, il faudrait vérifier le type d'erreur exact. log.Printf("Erreur de la base de données ou aucune valeur trouvée: %v", err) c.JSON(http.StatusNotFound, gin.H{"error": "Aucune donnée d'altitude trouvée pour ces coordonnées."}) return } c.IndentedJSON(http.StatusOK, AltitudeResponse{Z: altitude}) } } // getCorrectedAltitude est le handler pour la nouvelle route. // Il prend une altitude mesurée et retourne l'altitude corrigée. func getCorrectedAltitude(db *sql.DB) gin.HandlerFunc { return func(c *gin.Context) { var input GpsCoordWithZ // On valide et on lie le JSON d'entrée à notre struct GpsCoordWithZ. if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Paramètres invalides: " + err.Error()}) return } // On appelle notre fonction utilitaire pour obtenir la correction orthométrique (N). correction, err := queryOrthometricCorrection(db, input.Lat, input.Lon, input.SrcProj, input.DstProj) if err != nil { log.Printf("Erreur de la base de données ou aucune valeur trouvée: %v", err) c.JSON(http.StatusNotFound, gin.H{"error": "Aucune donnée de correction trouvée pour ces coordonnées."}) return } // On calcule l'altitude corrigée (H = h - N). // input.Z est l'altitude ellipsoïdale (h). // correction est l'ondulation du géoïde (N). correctedAltitude := input.Z - correction c.IndentedJSON(http.StatusOK, CorrectedAltitudeResponse{CorrectedZ: correctedAltitude}) } } func main() { // --- Chargement de la configuration --- // 0. Configuration pour les variables d'environnement viper.SetEnvPrefix("APP") // Les variables devront commencer par "APP_" (ex: APP_SERVER_ADDRESS) // 1. Fichier de configuration de base viper.SetConfigName("config") // config.yaml viper.SetConfigType("yaml") viper.AddConfigPath(".") // Lecture du fichier de base. On ne s'arrête pas s'il n'est pas trouvé. if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { log.Fatalf("Erreur lors de la lecture du fichier de base config.yaml: %s", err) } } // 2. Surcharge par environnement (via la variable APP_ENV) if env := os.Getenv("APP_ENV"); env != "" { viper.SetConfigName("config." + env) // ex: config.prod.yaml // MergeInConfig fusionne la nouvelle configuration avec celle déjà chargée. // Les valeurs du nouveau fichier écrasent les anciennes. if err := viper.MergeInConfig(); err != nil { log.Printf("Avertissement : impossible de charger le fichier de configuration pour l'environnement '%s': %v", env, err) } } // 2a. Indiquer à Viper de lire les variables d'environnement APRÈS avoir lu les fichiers. viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) // Remplace les "." par des "_" (ex: server.address -> SERVER_ADDRESS) viper.AutomaticEnv() // 3. "Unmarshal" de la configuration finale dans la struct var config Config if err := viper.Unmarshal(&config); err != nil { log.Fatalf("Impossible de décoder la configuration dans la structure : %v", err) } // --- Fin du chargement de la configuration --- // Chaîne de connexion à votre base de données PostgreSQL. // Construite à partir de la configuration. connStr := fmt.Sprintf("user=%s password=%s dbname=%s host=%s port=%d sslmode=%s", config.Database.User, config.Database.Password, config.Database.Dbname, config.Database.Host, config.Database.Port, config.Database.Sslmode, ) // Connexion à la base de données db, err := sql.Open("postgres", connStr) if err != nil { log.Fatal("Impossible de se connecter à la base de données:", err) } else { log.Println("Connecté à la base de données PostgreSQL") } defer db.Close() router := gin.Default() // Servir les fichiers statiques (index.html, etc.) depuis le répertoire courant. router.StaticFS("/", http.Dir(".")) // La route est maintenant un POST pour recevoir un corps de requête JSON. router.POST("/getorthocorrec", getOrthometricCorrection(db)) // Nouvelle route pour obtenir l'altitude corrigée. router.POST("/getcorrectedaltitude", getCorrectedAltitude(db)) log.Printf("Démarrage du serveur sur %s", config.Server.Address) router.Run(config.Server.Address) }