diff --git a/config.prod.yaml b/config.prod.yaml
new file mode 100644
index 0000000..52382a3
--- /dev/null
+++ b/config.prod.yaml
@@ -0,0 +1,9 @@
+server:
+ address: "127.0.0.1:8080" # Plus sécurisé, n'écoute que sur l'interface locale
+
+database:
+ host: "prod-db.example.com"
+ user: "prod_user"
+ password: "a_very_secret_password" # Idéalement, à gérer via un secret manager
+ port: 5432
+ sslmode: "require"
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..22ccf26
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,10 @@
+server:
+ address: "0.0.0.0:8080"
+
+database:
+ user: "realz"
+ password: "realz"
+ dbname: "realz"
+ host: "localhost"
+ port: 5433
+ sslmode: "disable"
diff --git a/docker/Dockerfile b/docker/Dockerfile.db
similarity index 100%
rename from docker/Dockerfile
rename to docker/Dockerfile.db
diff --git a/docker/compose.yaml b/docker/compose.yaml
index b99cb5b..133c6b2 100644
--- a/docker/compose.yaml
+++ b/docker/compose.yaml
@@ -1,4 +1,13 @@
services:
+ api:
+ image: postgrest/postgrest
+ ports:
+ - "3300:3000"
+ environment:
+ PGRST_DB_URI: postgres://${PGRST_AUTHUSER}:${PGRST_PASSWORD}@postgis:5432
+ PGRST_DB_SCHEMAS: raf20lambert93
+ PGRST_DB_ANON_ROLE: web_anon
+ PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
postgis:
image: realz
container_name: postgis_initialized
@@ -6,6 +15,8 @@ services:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB} # Base de données pour l'administration, pas celle de l'app
+ - PGRST_AUTHUSER=${PGRST_AUTHUSER}
+ - PGRST_PASSWORD=${PGRST_PASSWORD}
ports:
- "5433:5432"
volumes:
diff --git a/docker/scripts/init-db.sh b/docker/scripts/init-db.sh
index 8f3718d..47976d0 100644
--- a/docker/scripts/init-db.sh
+++ b/docker/scripts/init-db.sh
@@ -11,5 +11,14 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
EOSQL
echo "Init database data with RGF93"
-raster2pgsql -s RGF93 -I -C -M /opt/RAF20_lambert93.tiff -F -t 100x100 public.raf20lamber93 | psql -U ${POSTGRES_USER} -d ${POSTGRES_DB}
+raster2pgsql -s EPSG:2154 -I -C -M /opt/RAF20_lambert93.tiff -F -t 100x100 public.raf20lambert93 | psql -U ${POSTGRES_USER} -d ${POSTGRES_DB}
exit $?
+
+echo "Create postgrest roles"
+
+psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
+ CREATE ROLE webanon nologin;
+ GRANT USAGE ON SCHEMA raf20lamber93 TO webanon;
+ GRANT SELECT ON SCHEMA raf20lamber93 TO webanon;
+ GRANT web_anon to ${POSTGRES_USER}
+EOSQL
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..63a4982
--- /dev/null
+++ b/index.html
@@ -0,0 +1,171 @@
+
+
+
+
+
+ API getRealZ
+
+
+
+
+
+
Test de l'API Altitude
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Entrez des coordonnées et cliquez sur le bouton.
+
+
+
+
+
+
+
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..93ad434
--- /dev/null
+++ b/main.go
@@ -0,0 +1,202 @@
+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)
+}