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) +}