Release v3.1.4 - Mode terrain et génération PDF #10

Closed
Pierre wants to merge 15 commits from release/v3.1.4 into main
1438 changed files with 578989 additions and 12458 deletions
Showing only changes of commit 599b9fcda0 - Show all commits

135
.cline
View File

@@ -1,135 +0,0 @@
{
"memoryBank": {
"enabled": true,
"path": "./docs",
"maxContextSize": 100000
},
"contextSettings": {
"maxTokens": 100000,
"cline.maxAutoApprovedRequests": 100,
"cline.enableMemoryBank": true,
"cline.includeSnippetsFromMemory": true,
"cline.contextLength": 10000,
"cline.autoFormat": true
},
"mcpServers": {
"github.com/modelcontextprotocol/servers/tree/main/src/git": {
"command": "python",
"args": [
"-m",
"mcp_server_git"
],
"disabled": false,
"autoApprove": []
},
"github.com/GLips/Figma-Context-MCP": {
"command": "npx",
"args": [
"-y",
"figma-developer-mcp",
"--figma-api-key=figd_2SyOIL_LeFVIIpUuRFT6F2tNWPQl89lBmUWAnOsy",
"--stdio"
],
"disabled": false,
"autoApprove": []
}
},
"projectStructure": {
"api": {
"type": "php",
"version": "8.3",
"structure": [
"Controllers",
"Core",
"Services",
"Config",
"Utils"
],
"conventions": {
"classes": "PascalCase",
"methods": "camelCase",
"namespaces": "App\\*"
}
},
"app": {
"type": "flutter",
"version": "3.19",
"structure": [
"core",
"presentation",
"shared",
"chat"
],
"conventions": {
"classes": "PascalCase",
"methods": "camelCase",
"variables": "camelCase"
}
},
"web": {
"type": "svelte",
"version": "5",
"structure": [
"src/components",
"src/lib",
"src/pages"
],
"conventions": {
"components": "PascalCase.svelte",
"functions": "camelCase",
"stores": "camelCase"
}
}
},
"gitWorkflow": {
"mainBranch": "main",
"developBranch": "develop",
"featureBranchPrefix": "feature/",
"bugfixBranchPrefix": "bugfix/",
"releaseBranchPrefix": "release/",
"commitMessageFormat": "type(scope): message",
"commitTypes": [
"feat",
"fix",
"docs",
"style",
"refactor",
"test",
"chore"
],
"mergeStrategy": "fast-forward",
"requireCodeReview": true
},
"security": {
"sensitiveDataPatterns": [
"encrypted_email",
"encrypted_name",
"encrypted_phone",
"encrypted_mobile",
"encrypted_stripe_id",
"user_pass_hash"
],
"requireEncryption": true,
"avoidHardcodedCredentials": true,
"inputValidation": "required"
},
"documentation": {
"requireForPublicAPI": true,
"includeExamples": true,
"updateChangelogOnRelease": true,
"primaryDocumentationFile": "CONTEXT-AI.md",
"referenceFiles": {
"database": "docs/geo_app.dump",
"apiEndpoints": "docs/api_endpoints.md",
"architecture": "docs/architecture.md"
}
},
"codeQuality": {
"phpStandard": "PSR-12",
"dartAnalyzer": "strong-mode",
"eslintConfig": "recommended",
"maximumMethodLength": 50,
"maximumFileLength": 500,
"preferImmutability": true
}
}

0
CLAUDE.md Normal file → Executable file
View File

0
CONTEXT-AI.md Normal file → Executable file
View File

75
api/CLAUDE.md Executable file
View File

@@ -0,0 +1,75 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Directives importantes
- **Langue** : Toujours répondre en français
- **Approche de travail** :
- Travailler par étapes claires et structurées
- TOUJOURS présenter et proposer les modifications avant de les implémenter
- Attendre la validation de l'utilisateur avant de modifier le code
- Expliquer le problème identifié et la solution proposée
- **Vérification du schéma** : TOUJOURS vérifier `docs/geo_app.sql` avant de faire des modifications sur les tables de la base de données
## Build Commands
- Install dependencies: `composer install` - install PHP dependencies
- Update dependencies: `composer update` - update PHP dependencies to latest versions
- Deploy to REC: `./livre-api.sh rec` - deploy from DVA to RECETTE environment
- Deploy to PROD: `./livre-api.sh prod` - deploy from RECETTE to PRODUCTION environment
- Export operations: `php export_operation.php` - export operations data
## Code Architecture
This is a PHP 8.3 API without framework, using a custom MVC-like architecture:
- **Entry point**: `index.php` handles all requests through custom Router
- **Core components**:
- `Router`: Maps URLs to controller methods, handles HTTP methods
- `Database`: PDO wrapper for MariaDB connections
- `Session`: Secure session management
- `Request/Response`: HTTP request/response handling
- **Controllers**: Located in `src/Controllers/`, handle business logic
- **Services**: Located in `src/Services/`, provide reusable functionality (logging, email, exports)
- **Configuration**: `src/Config/AppConfig.php` - singleton configuration management
## Key Patterns
- No framework dependency - pure PHP 8.3 with composer autoloading
- PDO for database access with prepared statements
- RESTful API design with JSON responses
- CORS handling for cross-origin requests
- Session-based authentication
- File uploads handled in `uploads/` directory
- Logs stored in `logs/` directory
## Database
- MariaDB 10.11 with InnoDB tables
- Migration scripts in `scripts/php/migrate_*.php`
- Schema comparison tool: `scripts/python/compare_schemas.py`
- Database sync: `scripts/cron/sync_databases.php`
## Security Considerations
- Session cookies with httponly, secure flags
- CORS configured for specific origins
- XSS, clickjacking protection headers
- PDO prepared statements for SQL injection prevention
- File upload validation in FileService
## Bonnes pratiques spécifiques
### Gestion des transactions PDO
- Toujours vérifier `$db->inTransaction()` avant d'appeler `rollBack()`
- Encadrer les opérations critiques dans des try/catch avec transaction
### Paramètres SQL
- Utiliser des noms de paramètres uniques dans les requêtes SQL
- Ne jamais réutiliser le même nom de paramètre plusieurs fois dans une requête
- Exemple : `:sector_polygon1`, `:sector_polygon2` au lieu de `:sector_polygon` répété
### Format des réponses API
- Les données doivent être placées à la racine de la réponse JSON, pas dans un groupe "data"
- Suivre le modèle de `LoginController` pour la structure des réponses
- Retourner des objets complets, pas seulement des IDs (ex: sector complet, pas sector_id)
### Gestion des sessions
- La session stocke `entity_id` depuis `fk_entite` lors du login
- Utiliser `Session::getEntityId()` pour récupérer l'ID de l'entité
- L'authentification utilise des Bearer tokens contenant le session_id

View File

@@ -0,0 +1,13 @@
-- Modifier la table pour accepter tous les types de géométries (POLYGON et MULTIPOLYGON)
-- Option 1 : Modifier la colonne existante (recommandé)
ALTER TABLE x_departements_contours
MODIFY COLUMN contour GEOMETRY NOT NULL COMMENT 'Géométrie du contour du département (Polygon ou MultiPolygon)';
-- Vérifier la modification
DESCRIBE x_departements_contours;
-- Option 2 : Si l'option 1 ne fonctionne pas, recréer la colonne
-- ALTER TABLE x_departements_contours DROP COLUMN contour;
-- ALTER TABLE x_departements_contours ADD COLUMN contour GEOMETRY NOT NULL AFTER nom_dept;
-- ALTER TABLE x_departements_contours ADD SPATIAL INDEX idx_contour (contour);

0
api/bootstrap.php Normal file → Executable file
View File

0
api/composer.json Normal file → Executable file
View File

0
api/composer.lock generated Normal file → Executable file
View File

View File

@@ -0,0 +1,27 @@
-- Script de création de la table x_departements_contours
-- À exécuter manuellement en tant qu'administrateur de la base de données
CREATE TABLE IF NOT EXISTS `x_departements_contours` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code_dept` varchar(3) NOT NULL COMMENT 'Code département (22, 2A, 971...)',
`nom_dept` varchar(100) NOT NULL,
`contour` POLYGON NOT NULL COMMENT 'Polygone du contour du département',
`bbox_min_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude min de la bounding box',
`bbox_max_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude max de la bounding box',
`bbox_min_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude min de la bounding box',
`bbox_max_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude max de la bounding box',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_code_dept` (`code_dept`),
SPATIAL KEY `idx_contour` (`contour`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Contours géographiques des départements français';
-- Index pour améliorer les performances des requêtes par bounding box
CREATE INDEX idx_dept_bbox ON x_departements_contours (bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng);
-- Vérifier que la table a été créée
SHOW CREATE TABLE x_departements_contours\G
-- Vérifier les index
SHOW INDEX FROM x_departements_contours;

View File

@@ -10,7 +10,7 @@ set -euo pipefail
JUMP_USER="root" JUMP_USER="root"
JUMP_HOST="195.154.80.116" JUMP_HOST="195.154.80.116"
JUMP_PORT="22" JUMP_PORT="22"
JUMP_KEY="/Users/pierre/.ssh/id_rsa_mbpi" JUMP_KEY="/home/pierre/.ssh/id_rsa_mbpi"
# Paramètres du container Incus # Paramètres du container Incus
INCUS_PROJECT=default INCUS_PROJECT=default
@@ -73,6 +73,7 @@ fi
# Étape 0: Définir le nom de l'archive # Étape 0: Définir le nom de l'archive
ARCHIVE_NAME="api-deploy-$(date +%s).tar.gz" ARCHIVE_NAME="api-deploy-$(date +%s).tar.gz"
TEMP_ARCHIVE="/tmp/${ARCHIVE_NAME}"
echo_info "Archive name will be: $ARCHIVE_NAME" echo_info "Archive name will be: $ARCHIVE_NAME"
# Étape 1: Créer une archive du projet # Étape 1: Créer une archive du projet
@@ -88,18 +89,24 @@ tar --exclude='.git' \
--exclude='.DS_Store' \ --exclude='.DS_Store' \
--exclude='README.md' \ --exclude='README.md' \
--exclude="*.tar.gz" \ --exclude="*.tar.gz" \
--exclude='node_modules' \
--exclude='vendor' \
--exclude='*.swp' \
--exclude='*.swo' \
--exclude='*~' \
--warning=no-file-changed \
--no-xattrs \ --no-xattrs \
-czf "${ARCHIVE_NAME}" . || echo_error "Failed to create archive" -czf "${TEMP_ARCHIVE}" . || echo_error "Failed to create archive"
# Vérifier la taille de l'archive # Vérifier la taille de l'archive
ARCHIVE_SIZE=$(du -h "${ARCHIVE_NAME}" | cut -f1) ARCHIVE_SIZE=$(du -h "${TEMP_ARCHIVE}" | cut -f1)
SSH_JUMP_CMD="ssh -i ${JUMP_KEY} -p ${JUMP_PORT} ${JUMP_USER}@${JUMP_HOST}" SSH_JUMP_CMD="ssh -i ${JUMP_KEY} -p ${JUMP_PORT} ${JUMP_USER}@${JUMP_HOST}"
# Étape 2: Copier l'archive vers le serveur de saut # Étape 2: Copier l'archive vers le serveur de saut
echo_step "Copying archive to jump server..." echo_step "Copying archive to jump server..."
echo_info "Archive size: $ARCHIVE_SIZE" echo_info "Archive size: $ARCHIVE_SIZE"
scp -i "${JUMP_KEY}" -P "${JUMP_PORT}" "${ARCHIVE_NAME}" "${JUMP_USER}@${JUMP_HOST}:/tmp/${ARCHIVE_NAME}" || echo_error "Failed to copy archive to jump server" scp -i "${JUMP_KEY}" -P "${JUMP_PORT}" "${TEMP_ARCHIVE}" "${JUMP_USER}@${JUMP_HOST}:/tmp/${ARCHIVE_NAME}" || echo_error "Failed to copy archive to jump server"
# Étape 3: Exécuter les commandes sur le serveur de saut pour déployer dans le container Incus # Étape 3: Exécuter les commandes sur le serveur de saut pour déployer dans le container Incus
echo_step "Deploying to Incus container..." echo_step "Deploying to Incus container..."
@@ -140,7 +147,7 @@ $SSH_JUMP_CMD "
" "
# Nettoyage local # Nettoyage local
rm -f "${ARCHIVE_NAME}" rm -f "${TEMP_ARCHIVE}"
# Résumé final # Résumé final
echo_step "Deployment completed successfully." echo_step "Deployment completed successfully."

0
api/docs/CDC.md Normal file → Executable file
View File

0
api/docs/EXPORT-SYSTEM.md Normal file → Executable file
View File

0
api/docs/FILE-SYSTEM-API.md Normal file → Executable file
View File

430
api/docs/GESTION-SECTORS.md Normal file
View File

@@ -0,0 +1,430 @@
# GESTION-SECTORS.md
## Vue d'ensemble
Ce document décrit le système de gestion des secteurs dans l'API Geosector, incluant la connexion aux bases de données d'adresses externes, la validation des limites départementales, et le processus complet de création de secteurs avec génération automatique des passages.
## Évolutions récentes
### Gestion des sessions
- La session stocke maintenant `entity_id` depuis `fk_entite` lors du login
- Méthode `Session::getEntityId()` disponible pour récupérer l'ID de l'entité
- Utilisation cohérente de l'entity_id dans toutes les opérations
### Gestion des passages orphelins
- Les passages avec `fk_sector = 0` sont automatiquement intégrés au nouveau secteur
- Évite les doublons pour les passages ayant déjà une `fk_adresse`
- Mise à jour atomique dans la transaction de création du secteur
## Architecture multi-bases
### Bases de données principales
1. **Base principale** (`geosector_app`)
- Contient toutes les tables de l'application
- Tables concernées : `ope_sectors`, `sectors_adresses`, `ope_pass`, `ope_users_sectors`, `x_departements_contours`
2. **Base adresses** (dans conteneurs Incus séparés)
- DVA : `dva-maria` (13.23.33.46) - base `adresses`
- RCA : `rca-maria` (13.23.33.36) - base `adresses`
- PRA : `pra-maria` (13.23.33.26) - base `adresses`
- Credentials : `adr_geo_user` / `d66,AdrGeoDev.User`
- Tables par département : `cp22`, `cp23`, etc.
### Configuration
Dans `src/Config/AppConfig.php` :
```php
'addresses_database' => [
'host' => '13.23.33.46', // Varie selon l'environnement
'name' => 'adresses',
'username' => 'adr_geo_user',
'password' => 'd66,AdrGeoDev.User',
],
```
## Gestion des contours départementaux
### Table x_departements_contours
Création manuelle de la table (sans DROP permissions) :
```sql
CREATE TABLE IF NOT EXISTS `x_departements_contours` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code_dept` varchar(3) NOT NULL,
`nom_dept` varchar(100) NOT NULL,
`contour` GEOMETRY NOT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `idx_code_dept` (`code_dept`),
SPATIAL KEY `idx_contour` (`contour`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='Contours géographiques des départements français';
```
### Import des contours
1. **Fichier source** : `docs/contour-des-departements.geojson` (depuis data.gouv.fr)
2. **Import automatique** : Uniquement lors de la connexion de l'admin `d6soft`
3. **Script** : `scripts/init_departements_contours.php`
4. **Résultat** : 96 départements importés avec support Polygon et MultiPolygon
## Services principaux
### AddressService
Gère la récupération des adresses depuis la base externe :
```php
class AddressService {
// Récupère toutes les adresses dans un polygone
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array
// Compte les adresses dans un polygone
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int
}
```
**Caractéristiques** :
- Détection automatique des départements touchés par le secteur
- Interrogation de toutes les tables cp{dept} concernées
- Gestion des secteurs multi-départements
### DepartmentBoundaryService
Vérifie les limites départementales des secteurs :
```php
class DepartmentBoundaryService {
// Vérifie si un secteur est contenu dans un département
public function checkSectorInDepartment(array $sectorCoordinates, string $departmentCode): array
// Liste tous les départements touchés par un secteur
public function getDepartmentsForSector(array $sectorCoordinates): array
}
```
**Retour type** :
```php
[
'is_contained' => bool,
'message' => string,
'intersecting_departments' => [
['code_dept' => '22', 'nom_dept' => 'Côtes-d\'Armor', 'percentage_overlap' => 75.5],
['code_dept' => '29', 'nom_dept' => 'Finistère', 'percentage_overlap' => 24.5]
]
]
```
## Processus de création de secteur
### 1. Structure du payload
```json
{
"user_id": 123,
"fk_entite": 45,
"operation_id": 789,
"sector": {
"id": 0,
"libelle": "Secteur Centre-Ville",
"color": "#FF5733",
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#..."
},
"users": [12, 34, 56, 78]
}
```
### 2. Étapes de création
1. **Validation** des données et de l'opération
2. **Vérification** des limites départementales (warning si débordement)
3. **Début de transaction** pour garantir la cohérence des données
4. **Insertion** du secteur dans `ope_sectors`
5. **Affectation** des utilisateurs dans `ope_users_sectors` avec :
- `fk_operation`, `fk_user`, `fk_sector`
- `created_at`, `fk_user_creat`, `chk_active = 1`
6. **Intégration des passages orphelins** :
- Recherche des passages avec `fk_sector = 0` dans le polygone
- Mise à jour de leur `fk_sector` vers le nouveau secteur
- Exclusion des passages ayant déjà une `fk_adresse`
7. **Récupération** des adresses via `AddressService`
8. **Stockage** des adresses dans `sectors_adresses`
9. **Création** des passages dans `ope_pass` pour chaque adresse :
- Affectés au premier utilisateur de la liste
- Avec toutes les FK nécessaires (entité, opération, secteur, user)
- Données d'adresse complètes
10. **Commit** de la transaction ou **rollback** en cas d'erreur
### 3. Réponse API pour CREATE
**Format standardisé** : Les données sont placées à la racine, sans groupe "data" intermédiaire.
```json
{
"status": "success",
"message": "Secteur créé avec succès",
"sector": {
"id": 123,
"libelle": "Secteur Centre-Ville",
"color": "#FF5733",
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#..."
},
"passages_sector": [
{
"id": 456,
"fk_operation": 789,
"fk_sector": 123,
"fk_user": 12,
"fk_type": 2,
"fk_adresse": "cp22.12345",
"passed_at": null,
"numero": "10",
"rue": "Rue de la Paix",
"rue_bis": "",
"ville": "Saint-Brieuc",
"residence": null,
"fk_habitat": null,
"appt": null,
"niveau": null,
"gps_lat": "48.117266",
"gps_lng": "-1.6777926",
"nom_recu": null,
"name": "", // Décrypté depuis encrypted_name
"remarque": null,
"email": "", // Décrypté depuis encrypted_email
"phone": "", // Décrypté depuis encrypted_phone
"montant": null,
"fk_type_reglement": null,
"email_erreur": null,
"nb_passages": null
}
],
"passages_integrated": 5, // Passages orphelins intégrés
"passages_created": 10, // Nouveaux passages créés
"users_sectors": [
{
"id": 12,
"first_name": "Jean",
"sect_name": "JDU",
"fk_sector": 123,
"name": "Dupont" // Décrypté depuis encrypted_name
}
]
}
```
### 4. Réponse API pour UPDATE
La réponse est identique à CREATE avec des compteurs supplémentaires :
```json
{
"status": "success",
"message": "Secteur modifié avec succès",
"sector": {
"id": 123,
"libelle": "Secteur Centre-Ville Modifié",
"color": "#FF5733",
"sector": "48.117266/-1.6777926#48.118500/-1.6750000#..."
},
"passages_sector": [
// Liste complète de TOUS les passages actuels du secteur
],
"passages_orphaned": 3, // Passages mis en orphelin (hors polygone)
"passages_updated": 5, // Passages mis à jour avec fk_adresse
"passages_created": 10, // Nouveaux passages créés
"passages_total": 25, // Nombre total de passages dans le secteur
"users_sectors": [
// Liste des utilisateurs affectés
]
}
```
**Notes importantes** :
- Les champs sensibles (name, email, phone) sont stockés cryptés et décryptés à la volée
- La structure est identique entre CREATE et UPDATE pour faciliter l'intégration
- Tous les champs sont retournés, même s'ils sont null
- Code HTTP : 201 pour CREATE, 200 pour UPDATE
## Gestion des secteurs multi-départements
### Détection automatique
Le système détecte automatiquement quand un secteur touche plusieurs départements :
1. **Analyse spatiale** : Utilisation de `ST_Intersects` pour identifier tous les départements touchés
2. **Calcul de pourcentage** : `ST_Area(ST_Intersection)` pour calculer le % de recouvrement
3. **Interrogation multi-tables** : Requête sur toutes les tables cp{dept} concernées
### Exemple de secteur multi-départements
```php
// Secteur à cheval sur 22 (Côtes-d'Armor) et 29 (Finistère)
$coordinates = [
[48.5778, -3.8280], // Morlaix (29)
[48.5778, -3.7280], // Vers l'est (22)
[48.4778, -3.7280],
[48.4778, -3.8280]
];
// Le système va automatiquement :
// 1. Détecter que le secteur touche 22 et 29
// 2. Interroger cp22 et cp29 pour les adresses
// 3. Créer les passages pour toutes les adresses trouvées
```
## Tables de données
### ope_sectors
- `id` : Identifiant unique
- `libelle` : Nom du secteur
- `color` : Couleur d'affichage
- `sector` : Coordonnées (format lat/lng#lat/lng#...)
- `fk_entite` : Lien vers l'entité
### sectors_adresses
- `fk_sector` : Lien vers le secteur
- `fk_address` : ID de l'adresse dans la base externe
- `numero`, `voie`, `code_postal`, `commune`
- `latitude`, `longitude`
### ope_pass (passages)
- `fk_entite`, `fk_operation`, `fk_sector`, `fk_user`
- `numero`, `voie`, `code_postal`, `commune`
- `latitude`, `longitude`
- `created_at`, `fk_user_creat`, `chk_active`
### ope_users_sectors
- `fk_operation` : Lien vers l'opération
- `fk_user` : Lien vers l'utilisateur (ope_users)
- `fk_sector` : Lien vers le secteur
- `created_at`, `fk_user_creat`, `chk_active`
## Logs et monitoring
Le système génère des logs détaillés pour :
- Nombre d'adresses trouvées par département
- Secteurs hors limites départementales
- Passages créés avec succès
- Erreurs de connexion aux bases d'adresses
- Performance des requêtes spatiales
## Scripts de test
- `test_sector_departments.php` : Test des limites départementales
- `test_addresses_connection.php` : Test de connexion à la base d'adresses
## Notes importantes
1. **Fail-safe** : La création de secteur continue même si la base d'adresses est inaccessible
2. **Transactions** :
- Toute la création est dans une transaction pour garantir la cohérence
- Toujours vérifier `inTransaction()` avant d'appeler `rollBack()`
- Gestion correcte des erreurs PDO avec try/catch
3. **Performance** : Les requêtes spatiales utilisent des index spatiaux pour optimiser les performances
4. **Modification de secteur** : Plus complexe car nécessite de gérer les passages existants (non implémenté)
5. **Paramètres SQL** : Utiliser des noms uniques pour éviter l'erreur "Invalid parameter number"
6. **Jointures** : Les données utilisateur viennent de la table `users`, pas `ope_users` (qui n'a pas nom/prenom)
## Bilan de la gestion des adresses et passages
### Vue d'ensemble du cycle de vie
```
Base Adresses (cp22, cp23...) → sectors_adresses → ope_pass
```
### 1. CRÉATION D'UN SECTEUR
#### Flux des données :
1. **Récupération des adresses** depuis la base externe (`AddressService`)
2. **Intégration des passages orphelins** (`fk_sector = NULL`) situés dans le polygone
3. **Stockage dans `sectors_adresses`** de toutes les adresses du polygone
4. **Création automatique de passages** (`ope_pass`) pour chaque adresse SAUF celles déjà utilisées par les passages orphelins
#### Détails :
- **Passages créés** : `fk_type = 2`, `encrypted_name = ''` (vide), affectés au premier utilisateur
- **Passages orphelins** : mis à jour avec le nouveau `fk_sector`
- **Évite les doublons** : les adresses déjà utilisées par des passages orphelins ne génèrent pas de nouveau passage
### 2. MISE À JOUR D'UN SECTEUR
#### Processus de mise à jour :
1. **Mise à jour des attributs** (libelle, color, sector)
2. **Mise à jour des membres affectés**
3. **Suppression/recréation des adresses** dans `sectors_adresses`
4. **Gestion intelligente des passages** via `updatePassagesForSector` :
#### Gestion des passages lors de l'UPDATE :
##### a) Vérification géographique des passages existants
- Pour chaque passage du secteur, vérification si ses coordonnées GPS sont dans le nouveau polygone
- **Si DANS le polygone** : Conservation du passage
- **Si HORS du polygone** : Mise en orphelin (`fk_sector = NULL`)
##### b) Traitement des nouvelles adresses
Pour chaque adresse dans `sectors_adresses` :
1. **Vérification primaire** : Recherche par `fk_adresse`
2. **Vérification secondaire** : Si pas trouvé, recherche par `numero`, `rue_bis`, `rue`, `ville`
- Si trouvé → Mise à jour du `fk_adresse` dans le(s) passage(s)
3. **Création** : Si aucun passage existant, création avec :
- `fk_type = 2`, `encrypted_name = ''`
- Affecté au premier utilisateur du secteur
- Toutes les données de l'adresse
### 3. SUPPRESSION D'UN SECTEUR
#### Traitement différencié des passages :
1. **Passages "non visités"** (`fk_type = 2` ET `encrypted_name` vide) :
- Suppression définitive de la base
- Ces passages correspondent aux adresses non visitées
2. **Passages "visités"** (tous les autres) :
- Mise à jour : `fk_sector = NULL`
- Deviennent des passages orphelins
- Conservent toutes leurs données (contact, montant, etc.)
#### Autres suppressions :
- Suppression des affectations membres (`ope_users_sectors`)
- Suppression des adresses (`sectors_adresses`)
- Suppression du secteur lui-même
### Tableau récapitulatif
| Action | sectors_adresses | ope_pass dans polygone | ope_pass hors polygone | Nouvelles adresses |
|--------|------------------|------------------------|------------------------|-------------------|
| CREATE | Insertion depuis base externe | - | Intégration si orphelins | Création automatique de passages |
| UPDATE | Suppression/recréation | Conservation | Mise en orphelin | Création si pas de passage existant |
| DELETE | Suppression totale | Suppression si non visités / Orphelin si visités | - | - |
### Points d'attention
1. **Cohérence géographique** : Lors d'un UPDATE, le système vérifie automatiquement et met en orphelin les passages hors du nouveau périmètre
2. **Passages orphelins** : Peuvent être réintégrés lors de la création d'un nouveau secteur englobant
3. **Mise à jour du fk_adresse** : Lors d'un UPDATE, les passages existants peuvent recevoir leur `fk_adresse` s'ils correspondent à une adresse
4. **Performance** : La création/mise à jour génère potentiellement des milliers de passages selon la densité d'adresses
## Erreurs communes et solutions
### "There is no active transaction"
- **Cause** : Appel à `rollBack()` sans transaction active
- **Solution** : Vérifier `$db->inTransaction()` avant rollback
### "Column not found: fk_address"
- **Cause** : La colonne s'appelle `fk_adresse` (avec 'e')
- **Solution** : Corriger les noms de colonnes dans les requêtes
### "Invalid parameter number"
- **Cause** : Réutilisation du même nom de paramètre dans une requête
- **Solution** : Utiliser des noms uniques (`:param1`, `:param2`, etc.)
### "Unknown column 'ou.nom'"
- **Cause** : La table `ope_users` n'a pas de colonnes nom/prenom
- **Solution** : Joindre avec la table `users` qui contient `encrypted_name` et `first_name`
### "Class 'ApiService' not found"
- **Cause** : Import manquant dans le controller
- **Solution** : Ajouter `use App\Services\ApiService;` et `require_once`

0
api/docs/README-UPLOAD.md Normal file → Executable file
View File

0
api/docs/TECHBOOK.md Normal file → Executable file
View File

0
api/docs/api-analysis.md Normal file → Executable file
View File

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,53 @@
# Départements limitrophes
Ce document liste les départements limitrophes pour chaque département français.
À utiliser pour remplir le champ `dept_limitrophes` dans la table `x_departements`.
## Format
Le champ `dept_limitrophes` contient une liste de codes départements séparés par des virgules.
Exemple : "22,35,56" pour un département limitrophe avec les Côtes-d'Armor (22), l'Ille-et-Vilaine (35) et le Morbihan (56).
## Liste par département
### Bretagne
- **22 - Côtes-d'Armor** : 29,35,56
- **29 - Finistère** : 22,56
- **35 - Ille-et-Vilaine** : 22,44,49,50,53,56
- **56 - Morbihan** : 22,29,35,44
### Pays de la Loire
- **44 - Loire-Atlantique** : 35,49,56,85
- **49 - Maine-et-Loire** : 35,37,44,53,72,79,85,86
- **53 - Mayenne** : 14,35,49,50,61,72
- **72 - Sarthe** : 14,27,28,37,41,49,53,61
- **85 - Vendée** : 17,44,49,79
### Normandie
- **14 - Calvados** : 27,50,53,61,72
- **27 - Eure** : 14,28,60,61,72,76,78,95
- **50 - Manche** : 14,35,53,61
- **61 - Orne** : 14,27,28,35,41,50,53,72
- **76 - Seine-Maritime** : 27,60,80
### Île-de-France
- **75 - Paris** : 92,93,94
- **77 - Seine-et-Marne** : 02,10,45,51,60,89,91,93,94,95
- **78 - Yvelines** : 27,28,91,92,95
- **91 - Essonne** : 28,45,77,78,92,94
- **92 - Hauts-de-Seine** : 75,78,91,93,94,95
- **93 - Seine-Saint-Denis** : 75,77,92,94,95
- **94 - Val-de-Marne** : 75,77,91,92,93
- **95 - Val-d'Oise** : 27,60,77,78,92,93
### Hauts-de-France
- **02 - Aisne** : 08,51,59,60,77,80
- **59 - Nord** : 02,62,80 (+ frontière Belgique)
- **60 - Oise** : 02,27,76,77,80,95
- **62 - Pas-de-Calais** : 59,80 (+ frontière Belgique et côte Manche)
- **80 - Somme** : 02,27,59,60,62,76
## Notes
- Cette liste est à compléter pour tous les départements français
- Les départements d'outre-mer n'ont généralement pas de départements limitrophes terrestres
- Certains départements peuvent avoir des limites maritimes non représentées ici
- Source recommandée : données INSEE ou IGN pour une liste complète et exacte

0
api/docs/flowIncus.md Normal file → Executable file
View File

0
api/docs/geo_app.sql Normal file → Executable file
View File

0
api/docs/geosector-db-diagram.md Normal file → Executable file
View File

0
api/docs/geosector_app.sql Normal file → Executable file
View File

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

0
api/export_operation.php Normal file → Executable file
View File

10
api/index.php Normal file → Executable file
View File

@@ -7,6 +7,7 @@ require_once __DIR__ . '/bootstrap.php';
// Chargement des fichiers principaux // Chargement des fichiers principaux
require_once __DIR__ . '/src/Config/AppConfig.php'; require_once __DIR__ . '/src/Config/AppConfig.php';
require_once __DIR__ . '/src/Core/Database.php'; require_once __DIR__ . '/src/Core/Database.php';
require_once __DIR__ . '/src/Core/AddressesDatabase.php';
require_once __DIR__ . '/src/Core/Router.php'; require_once __DIR__ . '/src/Core/Router.php';
require_once __DIR__ . '/src/Core/Session.php'; require_once __DIR__ . '/src/Core/Session.php';
require_once __DIR__ . '/src/Core/Request.php'; require_once __DIR__ . '/src/Core/Request.php';
@@ -20,14 +21,21 @@ require_once __DIR__ . '/src/Controllers/LoginController.php';
require_once __DIR__ . '/src/Controllers/EntiteController.php'; require_once __DIR__ . '/src/Controllers/EntiteController.php';
require_once __DIR__ . '/src/Controllers/UserController.php'; require_once __DIR__ . '/src/Controllers/UserController.php';
require_once __DIR__ . '/src/Controllers/OperationController.php'; require_once __DIR__ . '/src/Controllers/OperationController.php';
require_once __DIR__ . '/src/Controllers/PassageController.php';
require_once __DIR__ . '/src/Controllers/VilleController.php';
require_once __DIR__ . '/src/Controllers/FileController.php';
require_once __DIR__ . '/src/Controllers/SectorController.php';
// Initialiser la configuration // Initialiser la configuration
$appConfig = AppConfig::getInstance(); $appConfig = AppConfig::getInstance();
$config = $appConfig->getFullConfig(); $config = $appConfig->getFullConfig();
// Initialiser la base de données // Initialiser la base de données principale
Database::init($config['database']); Database::init($config['database']);
// Initialiser la base de données des adresses
AddressesDatabase::init($appConfig->getAddressesDatabaseConfig());
// Configuration CORS // Configuration CORS
$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; $origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$allowedOrigins = $config['api']['allowed_origins']; $allowedOrigins = $config['api']['allowed_origins'];

View File

@@ -14,18 +14,18 @@ fi
HOST_IP="195.154.80.116" HOST_IP="195.154.80.116"
HOST_USER=root HOST_USER=root
HOST_KEY=/Users/pierre/.ssh/id_rsa_mbpi HOST_KEY=/home/pierre/.ssh/id_rsa_mbpi
HOST_PORT=22 HOST_PORT=22
# Mapping des environnements # Mapping des environnements
ENVIRONMENT=$1 ENVIRONMENT=$1
case $ENVIRONMENT in case $ENVIRONMENT in
"rec") "rca")
SOURCE_CONTAINER="dva-geo" SOURCE_CONTAINER="dva-geo"
DEST_CONTAINER="rca-geo" DEST_CONTAINER="rca-geo"
ENV_NAME="RECETTE" ENV_NAME="RECETTE"
;; ;;
"prod") "pra")
SOURCE_CONTAINER="rca-geo" SOURCE_CONTAINER="rca-geo"
DEST_CONTAINER="pra-geo" DEST_CONTAINER="pra-geo"
ENV_NAME="PRODUCTION" ENV_NAME="PRODUCTION"

View File

@@ -0,0 +1,28 @@
-- Table pour stocker les contours (polygones) des départements
CREATE TABLE IF NOT EXISTS `x_departements_contours` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code_dept` varchar(3) NOT NULL COMMENT 'Code département (22, 2A, 971...)',
`nom_dept` varchar(100) NOT NULL,
`contour` POLYGON NOT NULL COMMENT 'Polygone du contour du département',
`bbox_min_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude min de la bounding box',
`bbox_max_lat` decimal(10,8) DEFAULT NULL COMMENT 'Latitude max de la bounding box',
`bbox_min_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude min de la bounding box',
`bbox_max_lng` decimal(11,8) DEFAULT NULL COMMENT 'Longitude max de la bounding box',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_code_dept` (`code_dept`),
SPATIAL KEY `idx_contour` (`contour`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Index pour améliorer les performances
CREATE INDEX idx_dept_bbox ON x_departements_contours (bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng);
-- Exemples d'insertion (à remplacer par les vraies données)
-- Les coordonnées doivent être dans l'ordre : longitude latitude
-- Le polygone doit être fermé (premier point = dernier point)
/*
INSERT INTO x_departements_contours (code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng) VALUES
('22', 'Côtes-d\'Armor', ST_GeomFromText('POLYGON((-3.65 48.90, -2.00 48.90, -2.00 48.05, -3.65 48.05, -3.65 48.90))'), 48.05, 48.90, -3.65, -2.00),
('29', 'Finistère', ST_GeomFromText('POLYGON((-5.14 48.75, -3.38 48.75, -3.38 47.64, -5.14 47.64, -5.14 48.75))'), 47.64, 48.75, -5.14, -3.38);
*/

0
api/migration_add_file_category.sql Normal file → Executable file
View File

0
api/migration_add_ope_users_fields.sql Normal file → Executable file
View File

View File

@@ -0,0 +1,29 @@
-- Migration pour ajouter la table sectors_adresses
-- Cette table stocke les adresses associées à chaque secteur
CREATE TABLE IF NOT EXISTS `sectors_adresses` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`fk_sector` int(11) NOT NULL,
`fk_address` bigint(20) NOT NULL COMMENT 'ID de l''adresse dans la base adresses',
`numero` varchar(10) DEFAULT NULL,
`voie` varchar(255) DEFAULT NULL,
`code_postal` varchar(5) DEFAULT NULL,
`commune` varchar(100) DEFAULT NULL,
`latitude` decimal(10,8) NOT NULL,
`longitude` decimal(11,8) NOT NULL,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sector` (`fk_sector`),
KEY `idx_address` (`fk_address`),
KEY `idx_code_postal` (`code_postal`),
KEY `idx_commune` (`commune`),
KEY `idx_coords` (`latitude`, `longitude`),
CONSTRAINT `fk_sectors_adresses_sector` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Index pour améliorer les performances des requêtes géographiques
CREATE INDEX idx_sectors_adresses_geo ON sectors_adresses (fk_sector, latitude, longitude);
-- Commentaire sur la table
ALTER TABLE `sectors_adresses`
COMMENT = 'Table de liaison entre les secteurs et les adresses géographiques contenues dans leurs périmètres';

View File

@@ -0,0 +1,45 @@
-- Ajout du champ dept_limitrophes dans la table x_departements
-- Ce champ contiendra la liste des codes départements limitrophes séparés par des virgules
-- Exemple : "22,35,44,56" pour le Morbihan (56)
ALTER TABLE x_departements
ADD COLUMN dept_limitrophes VARCHAR(100) DEFAULT NULL
COMMENT 'Liste des codes départements limitrophes séparés par des virgules'
AFTER libelle;
-- Exemples de mise à jour pour quelques départements bretons
-- À compléter avec tous les départements
-- Côtes-d'Armor (22) : limitrophe avec 29, 35, 56
UPDATE x_departements SET dept_limitrophes = '29,35,56' WHERE code = '22';
-- Finistère (29) : limitrophe avec 22, 56
UPDATE x_departements SET dept_limitrophes = '22,56' WHERE code = '29';
-- Ille-et-Vilaine (35) : limitrophe avec 22, 44, 49, 50, 53, 56
UPDATE x_departements SET dept_limitrophes = '22,44,49,50,53,56' WHERE code = '35';
-- Morbihan (56) : limitrophe avec 22, 29, 35, 44
UPDATE x_departements SET dept_limitrophes = '22,29,35,44' WHERE code = '56';
-- Loire-Atlantique (44) : limitrophe avec 35, 49, 56, 85
UPDATE x_departements SET dept_limitrophes = '35,49,56,85' WHERE code = '44';
-- Hauts-de-France
-- Aisne (02) : limitrophe avec 08, 51, 59, 60, 77, 80
UPDATE x_departements SET dept_limitrophes = '08,51,59,60,77,80' WHERE code = '02';
-- Nord (59) : limitrophe avec 02, 62, 80 (+ frontière Belgique)
UPDATE x_departements SET dept_limitrophes = '02,62,80' WHERE code = '59';
-- Oise (60) : limitrophe avec 02, 27, 76, 77, 80, 95
UPDATE x_departements SET dept_limitrophes = '02,27,76,77,80,95' WHERE code = '60';
-- Pas-de-Calais (62) : limitrophe avec 59, 80
UPDATE x_departements SET dept_limitrophes = '59,80' WHERE code = '62';
-- Somme (80) : limitrophe avec 02, 27, 59, 60, 62, 76
UPDATE x_departements SET dept_limitrophes = '02,27,59,60,62,76' WHERE code = '80';
-- Note : Ces données sont à compléter pour tous les départements français
-- Source recommandée : données INSEE ou IGN pour la liste complète et exacte

View File

@@ -0,0 +1,55 @@
-- Script pour intégrer les données de x_departements_contours dans x_departements
-- Ce script ajoute une colonne contour à x_departements et y copie les données
-- Note : Les colonnes bbox_* ne sont pas migrées car elles ne sont pas utilisées dans l'API
-- 1. Ajouter la colonne contour à x_departements si elle n'existe pas déjà
ALTER TABLE x_departements
ADD COLUMN IF NOT EXISTS contour GEOMETRY DEFAULT NULL
COMMENT 'Contour géographique du département'
AFTER dept_limitrophes;
-- 2. Mettre à jour x_departements avec les contours depuis x_departements_contours
-- On copie directement les contours non NULL
UPDATE x_departements d
INNER JOIN x_departements_contours dc ON d.code = dc.code_dept
SET d.contour = dc.contour
WHERE dc.contour IS NOT NULL;
-- 3. Vérifier les départements sans contour (hors DOM-TOM et Corse historique)
SELECT
d.code,
d.libelle,
CASE
WHEN d.code IN ('20', '971', '972', '973', '974', '975', '976') THEN 'DOM-TOM ou Corse historique - Normal'
WHEN d.contour IS NULL THEN 'Pas de contour'
ELSE 'OK'
END as statut
FROM x_departements d
WHERE d.contour IS NULL
AND d.code NOT IN ('20', '971', '972', '973', '974', '975', '976')
ORDER BY d.code;
-- 4. Créer l'index spatial
-- Les valeurs NULL sont autorisées dans un index spatial MySQL
ALTER TABLE x_departements
ADD SPATIAL INDEX idx_contour (contour);
-- 5. Vérifier le nombre de départements mis à jour
SELECT
COUNT(*) as total_departements,
SUM(CASE WHEN contour IS NOT NULL THEN 1 ELSE 0 END) as departements_avec_contour,
SUM(CASE WHEN contour IS NULL THEN 1 ELSE 0 END) as departements_sans_contour
FROM x_departements;
-- 6. Lister les départements qui n'ont pas de contour (s'il y en a)
SELECT code, libelle
FROM x_departements
WHERE contour IS NULL
ORDER BY code;
-- 7. Optionnel : Après vérification, vous pouvez supprimer la table x_departements_contours
-- ATTENTION : Ne décommentez cette ligne qu'après avoir vérifié que toutes les données sont bien migrées
-- DROP TABLE IF EXISTS x_departements_contours;
-- 8. Mettre à jour les statistiques de la table pour optimiser les requêtes spatiales
ANALYZE TABLE x_departements;

View File

@@ -0,0 +1,131 @@
-- Mise à jour complète des départements limitrophes pour tous les départements français
-- Format : liste des codes départements séparés par des virgules
-- Auvergne-Rhône-Alpes
UPDATE x_departements SET dept_limitrophes = '07,15,43,48' WHERE code = '01'; -- Ain : Ardèche, Cantal, Haute-Loire, Lozère
UPDATE x_departements SET dept_limitrophes = '04,05,26,84' WHERE code = '03'; -- Allier : Alpes-de-Haute-Provence, Hautes-Alpes, Drôme, Vaucluse
UPDATE x_departements SET dept_limitrophes = '01,26,30,43,48,84' WHERE code = '07'; -- Ardèche : Ain, Drôme, Gard, Haute-Loire, Lozère, Vaucluse
UPDATE x_departements SET dept_limitrophes = '12,19,43,46,48' WHERE code = '15'; -- Cantal : Aveyron, Corrèze, Haute-Loire, Lot, Lozère
UPDATE x_departements SET dept_limitrophes = '04,05,73,84' WHERE code = '26'; -- Drôme : Alpes-de-Haute-Provence, Hautes-Alpes, Savoie, Vaucluse
UPDATE x_departements SET dept_limitrophes = '05,06,13,73' WHERE code = '38'; -- Isère : Hautes-Alpes, Alpes-Maritimes, Bouches-du-Rhône, Savoie
UPDATE x_departements SET dept_limitrophes = '07,15,19,43,48,63' WHERE code = '42'; -- Loire : Ardèche, Cantal, Corrèze, Haute-Loire, Lozère, Puy-de-Dôme
UPDATE x_departements SET dept_limitrophes = '01,07,15,42,48,63' WHERE code = '43'; -- Haute-Loire : Ain, Ardèche, Cantal, Loire, Lozère, Puy-de-Dôme
UPDATE x_departements SET dept_limitrophes = '03,15,23,42,43' WHERE code = '63'; -- Puy-de-Dôme : Allier, Cantal, Creuse, Loire, Haute-Loire
UPDATE x_departements SET dept_limitrophes = '01,38,39,71' WHERE code = '69'; -- Rhône : Ain, Isère, Jura, Saône-et-Loire
UPDATE x_departements SET dept_limitrophes = '01,25,38,39,74' WHERE code = '73'; -- Savoie : Ain, Doubs, Isère, Jura, Haute-Savoie
UPDATE x_departements SET dept_limitrophes = '01,73' WHERE code = '74'; -- Haute-Savoie : Ain, Savoie (+ frontières Suisse et Italie)
-- Bourgogne-Franche-Comté
UPDATE x_departements SET dept_limitrophes = '10,45,52,58,77,89' WHERE code = '21'; -- Côte-d'Or : Aube, Loiret, Haute-Marne, Nièvre, Seine-et-Marne, Yonne
UPDATE x_departements SET dept_limitrophes = '39,68,70,73,90' WHERE code = '25'; -- Doubs : Jura, Haut-Rhin, Haute-Saône, Savoie, Territoire de Belfort (+ frontière Suisse)
UPDATE x_departements SET dept_limitrophes = '01,25,69,70,71,73' WHERE code = '39'; -- Jura : Ain, Doubs, Rhône, Haute-Saône, Saône-et-Loire, Savoie
UPDATE x_departements SET dept_limitrophes = '03,18,21,45,71,89' WHERE code = '58'; -- Nièvre : Allier, Cher, Côte-d'Or, Loiret, Saône-et-Loire, Yonne
UPDATE x_departements SET dept_limitrophes = '21,25,39,52,88' WHERE code = '70'; -- Haute-Saône : Côte-d'Or, Doubs, Jura, Haute-Marne, Vosges
UPDATE x_departements SET dept_limitrophes = '01,03,21,39,58,69' WHERE code = '71'; -- Saône-et-Loire : Ain, Allier, Côte-d'Or, Jura, Nièvre, Rhône
UPDATE x_departements SET dept_limitrophes = '10,21,45,58,77' WHERE code = '89'; -- Yonne : Aube, Côte-d'Or, Loiret, Nièvre, Seine-et-Marne
UPDATE x_departements SET dept_limitrophes = '25,68,70' WHERE code = '90'; -- Territoire de Belfort : Doubs, Haut-Rhin, Haute-Saône
-- Bretagne
UPDATE x_departements SET dept_limitrophes = '29,35,56' WHERE code = '22'; -- Côtes-d'Armor
UPDATE x_departements SET dept_limitrophes = '22,56' WHERE code = '29'; -- Finistère
UPDATE x_departements SET dept_limitrophes = '22,44,49,50,53,56' WHERE code = '35'; -- Ille-et-Vilaine
UPDATE x_departements SET dept_limitrophes = '22,29,35,44' WHERE code = '56'; -- Morbihan
-- Centre-Val de Loire
UPDATE x_departements SET dept_limitrophes = '03,23,36,41,58' WHERE code = '18'; -- Cher
UPDATE x_departements SET dept_limitrophes = '27,37,41,45,61,72,78,91' WHERE code = '28'; -- Eure-et-Loir
UPDATE x_departements SET dept_limitrophes = '18,23,37,41,86,87' WHERE code = '36'; -- Indre
UPDATE x_departements SET dept_limitrophes = '36,41,49,72,86' WHERE code = '37'; -- Indre-et-Loire
UPDATE x_departements SET dept_limitrophes = '18,28,36,37,45,72' WHERE code = '41'; -- Loir-et-Cher
UPDATE x_departements SET dept_limitrophes = '18,21,28,41,58,77,89,91' WHERE code = '45'; -- Loiret
-- Corse
UPDATE x_departements SET dept_limitrophes = '2B' WHERE code = '2A'; -- Corse-du-Sud : Haute-Corse
UPDATE x_departements SET dept_limitrophes = '2A' WHERE code = '2B'; -- Haute-Corse : Corse-du-Sud
-- Grand Est
UPDATE x_departements SET dept_limitrophes = '02,51,55' WHERE code = '08'; -- Ardennes (+ frontière Belgique)
UPDATE x_departements SET dept_limitrophes = '21,51,52,77,89' WHERE code = '10'; -- Aube
UPDATE x_departements SET dept_limitrophes = '02,08,10,52,77' WHERE code = '51'; -- Marne
UPDATE x_departements SET dept_limitrophes = '10,21,51,55,70,88' WHERE code = '52'; -- Haute-Marne
UPDATE x_departements SET dept_limitrophes = '55,57,88' WHERE code = '54'; -- Meurthe-et-Moselle
UPDATE x_departements SET dept_limitrophes = '08,52,54,57' WHERE code = '55'; -- Meuse (+ frontière Belgique)
UPDATE x_departements SET dept_limitrophes = '54,55,67' WHERE code = '57'; -- Moselle (+ frontières Luxembourg et Allemagne)
UPDATE x_departements SET dept_limitrophes = '25,57,68,88,90' WHERE code = '67'; -- Bas-Rhin (+ frontière Allemagne)
UPDATE x_departements SET dept_limitrophes = '67,70,88,90' WHERE code = '68'; -- Haut-Rhin (+ frontières Allemagne et Suisse)
UPDATE x_departements SET dept_limitrophes = '52,54,67,68,70' WHERE code = '88'; -- Vosges
-- Hauts-de-France
UPDATE x_departements SET dept_limitrophes = '08,51,59,60,77,80' WHERE code = '02'; -- Aisne
UPDATE x_departements SET dept_limitrophes = '02,62,80' WHERE code = '59'; -- Nord (+ frontière Belgique)
UPDATE x_departements SET dept_limitrophes = '02,27,76,77,80,95' WHERE code = '60'; -- Oise
UPDATE x_departements SET dept_limitrophes = '59,80' WHERE code = '62'; -- Pas-de-Calais (+ frontière Belgique)
UPDATE x_departements SET dept_limitrophes = '02,27,59,60,62,76' WHERE code = '80'; -- Somme
-- Île-de-France
UPDATE x_departements SET dept_limitrophes = '92,93,94' WHERE code = '75'; -- Paris
UPDATE x_departements SET dept_limitrophes = '02,10,45,51,60,89,91,93,94,95' WHERE code = '77'; -- Seine-et-Marne
UPDATE x_departements SET dept_limitrophes = '27,28,91,92,95' WHERE code = '78'; -- Yvelines
UPDATE x_departements SET dept_limitrophes = '28,45,77,78,92,94' WHERE code = '91'; -- Essonne
UPDATE x_departements SET dept_limitrophes = '75,78,91,93,94,95' WHERE code = '92'; -- Hauts-de-Seine
UPDATE x_departements SET dept_limitrophes = '75,77,92,94,95' WHERE code = '93'; -- Seine-Saint-Denis
UPDATE x_departements SET dept_limitrophes = '75,77,91,92,93' WHERE code = '94'; -- Val-de-Marne
UPDATE x_departements SET dept_limitrophes = '27,60,77,78,92,93' WHERE code = '95'; -- Val-d'Oise
-- Normandie
UPDATE x_departements SET dept_limitrophes = '27,50,53,61,72' WHERE code = '14'; -- Calvados
UPDATE x_departements SET dept_limitrophes = '14,28,60,61,72,76,78,95' WHERE code = '27'; -- Eure
UPDATE x_departements SET dept_limitrophes = '14,35,53,61' WHERE code = '50'; -- Manche
UPDATE x_departements SET dept_limitrophes = '14,27,28,35,41,50,53,72' WHERE code = '61'; -- Orne
UPDATE x_departements SET dept_limitrophes = '27,60,80' WHERE code = '76'; -- Seine-Maritime
-- Nouvelle-Aquitaine
UPDATE x_departements SET dept_limitrophes = '17,24,33,87' WHERE code = '16'; -- Charente
UPDATE x_departements SET dept_limitrophes = '16,33,79,85' WHERE code = '17'; -- Charente-Maritime
UPDATE x_departements SET dept_limitrophes = '15,23,24,46,87' WHERE code = '19'; -- Corrèze
UPDATE x_departements SET dept_limitrophes = '18,19,36,86,87' WHERE code = '23'; -- Creuse
UPDATE x_departements SET dept_limitrophes = '16,19,33,46,47,87' WHERE code = '24'; -- Dordogne
UPDATE x_departements SET dept_limitrophes = '16,17,24,40,47' WHERE code = '33'; -- Gironde
UPDATE x_departements SET dept_limitrophes = '32,33,47,64,65' WHERE code = '40'; -- Landes
UPDATE x_departements SET dept_limitrophes = '24,32,40,46,82' WHERE code = '47'; -- Lot-et-Garonne
UPDATE x_departements SET dept_limitrophes = '40,65' WHERE code = '64'; -- Pyrénées-Atlantiques (+ frontière Espagne)
UPDATE x_departements SET dept_limitrophes = '17,49,85,86' WHERE code = '79'; -- Deux-Sèvres
UPDATE x_departements SET dept_limitrophes = '16,23,36,37,79' WHERE code = '86'; -- Vienne
UPDATE x_departements SET dept_limitrophes = '16,19,23,24,36,86' WHERE code = '87'; -- Haute-Vienne
-- Occitanie
UPDATE x_departements SET dept_limitrophes = '11,31,66' WHERE code = '09'; -- Ariège : Aude, Haute-Garonne, Pyrénées-Orientales (+ frontières Espagne et Andorre)
UPDATE x_departements SET dept_limitrophes = '09,31,66' WHERE code = '11'; -- Aude
UPDATE x_departements SET dept_limitrophes = '15,30,34,46,48,81,82' WHERE code = '12'; -- Aveyron
UPDATE x_departements SET dept_limitrophes = '07,12,34,48,84' WHERE code = '30'; -- Gard
UPDATE x_departements SET dept_limitrophes = '09,32,65,82' WHERE code = '31'; -- Haute-Garonne : Ariège, Gers, Hautes-Pyrénées, Tarn-et-Garonne (+ frontière Espagne)
UPDATE x_departements SET dept_limitrophes = '31,40,47,65,82' WHERE code = '32'; -- Gers
UPDATE x_departements SET dept_limitrophes = '11,12,30' WHERE code = '34'; -- Hérault
UPDATE x_departements SET dept_limitrophes = '12,15,19,24,47,81,82' WHERE code = '46'; -- Lot
UPDATE x_departements SET dept_limitrophes = '07,12,15,30,43' WHERE code = '48'; -- Lozère
UPDATE x_departements SET dept_limitrophes = '31,32,40,64' WHERE code = '65'; -- Hautes-Pyrénées (+ frontière Espagne)
UPDATE x_departements SET dept_limitrophes = '09,11' WHERE code = '66'; -- Pyrénées-Orientales (+ frontière Espagne)
UPDATE x_departements SET dept_limitrophes = '12,34,46,82' WHERE code = '81'; -- Tarn
UPDATE x_departements SET dept_limitrophes = '12,31,32,46,47,81' WHERE code = '82'; -- Tarn-et-Garonne
-- Pays de la Loire
UPDATE x_departements SET dept_limitrophes = '35,49,56,85' WHERE code = '44'; -- Loire-Atlantique
UPDATE x_departements SET dept_limitrophes = '35,37,44,53,72,79,85,86' WHERE code = '49'; -- Maine-et-Loire
UPDATE x_departements SET dept_limitrophes = '14,35,49,50,61,72' WHERE code = '53'; -- Mayenne
UPDATE x_departements SET dept_limitrophes = '14,27,28,37,41,49,53,61' WHERE code = '72'; -- Sarthe
UPDATE x_departements SET dept_limitrophes = '17,44,49,79' WHERE code = '85'; -- Vendée
-- Provence-Alpes-Côte d'Azur
UPDATE x_departements SET dept_limitrophes = '05,06,26,83,84' WHERE code = '04'; -- Alpes-de-Haute-Provence
UPDATE x_departements SET dept_limitrophes = '04,26,38,73' WHERE code = '05'; -- Hautes-Alpes (+ frontière Italie)
UPDATE x_departements SET dept_limitrophes = '04,83' WHERE code = '06'; -- Alpes-Maritimes (+ frontières Italie et Monaco)
UPDATE x_departements SET dept_limitrophes = '30,83,84' WHERE code = '13'; -- Bouches-du-Rhône
UPDATE x_departements SET dept_limitrophes = '04,06,13,84' WHERE code = '83'; -- Var
UPDATE x_departements SET dept_limitrophes = '04,07,13,26,30,83' WHERE code = '84'; -- Vaucluse
-- Départements et régions d'outre-mer (pas de limitrophes terrestres)
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '971'; -- Guadeloupe
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '972'; -- Martinique
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '973'; -- Guyane (frontières Brésil et Suriname)
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '974'; -- La Réunion
UPDATE x_departements SET dept_limitrophes = NULL WHERE code = '976'; -- Mayotte

0
api/scripts/README.md Normal file → Executable file
View File

View File

@@ -0,0 +1,33 @@
-- Script de diagnostic pour vérifier les problèmes de géométrie dans x_departements_contours
-- 1. Vérifier les contours NULL
SELECT 'Contours NULL:' as diagnostic;
SELECT code_dept, nom_dept
FROM x_departements_contours
WHERE contour IS NULL;
-- 2. Vérifier les types de géométrie et si elles sont vides
SELECT 'Types de géométrie:' as diagnostic;
SELECT
code_dept,
nom_dept,
ST_GeometryType(contour) as geometry_type,
ST_IsEmpty(contour) as is_empty
FROM x_departements_contours
WHERE contour IS NOT NULL;
-- 3. Statistiques générales
SELECT 'Statistiques:' as diagnostic;
SELECT
COUNT(*) as total,
SUM(CASE WHEN contour IS NULL THEN 1 ELSE 0 END) as contours_null,
SUM(CASE WHEN contour IS NOT NULL THEN 1 ELSE 0 END) as contours_non_null
FROM x_departements_contours;
-- 4. Lister spécifiquement les DOM-TOM et Corse
SELECT 'DOM-TOM et Corse:' as diagnostic;
SELECT code_dept, nom_dept,
CASE WHEN contour IS NULL THEN 'NULL' ELSE 'OK' END as contour_status
FROM x_departements_contours
WHERE code_dept IN ('20', '2A', '2B', '971', '972', '973', '974', '975', '976')
ORDER BY code_dept;

0
api/scripts/config.php Normal file → Executable file
View File

View File

@@ -0,0 +1,32 @@
-- Script de création des utilisateurs pour la base de données des adresses
-- À exécuter sur chaque serveur MariaDB (dva-maria, rca-maria, pra-maria)
-- Créer l'utilisateur avec accès depuis l'IP du container API correspondant
-- IMPORTANT: Remplacer 'API_CONTAINER_IP' par l'IP réelle du container API
-- Pour l'environnement DEV (dva-maria)
-- Si l'API est dans le container dva-api avec l'IP 13.23.33.45 par exemple :
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.45' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.45';
-- Pour l'environnement RECETTE (rca-maria)
-- Si l'API est dans le container rca-api avec l'IP 13.23.33.35 par exemple :
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.35' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.35';
-- Pour l'environnement PROD (pra-maria)
-- Si l'API est dans le container pra-api avec l'IP 13.23.33.25 par exemple :
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.25' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.25';
-- Alternative : Créer un utilisateur accessible depuis tout le sous-réseau
-- ATTENTION : Moins sécurisé, à utiliser uniquement si les containers sont dans un réseau privé isolé
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.%' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.%';
-- Appliquer les privilèges
FLUSH PRIVILEGES;
-- Vérifier la création
SELECT user, host FROM mysql.user WHERE user = 'adresses_user';
SHOW GRANTS FOR 'adresses_user'@'13.23.33.%';

View File

@@ -0,0 +1,41 @@
-- Script de création des utilisateurs pour la base de données des adresses
-- Avec segmentation par environnement basée sur les plages d'IPs
-- ===================================
-- DÉVELOPPEMENT (dva-maria)
-- IPs autorisées : 13.23.33.40-49
-- ===================================
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.4%' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.4%';
-- Aussi créer un accès localhost pour les tests directs
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
-- ===================================
-- RECETTE (rca-maria)
-- IPs autorisées : 13.23.33.30-39
-- ===================================
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.3%' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.3%';
-- Aussi créer un accès localhost pour les tests directs
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
-- ===================================
-- PRODUCTION (pra-maria)
-- IPs autorisées : 13.23.33.20-29
-- ===================================
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.2%' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.2%';
-- Aussi créer un accès localhost pour les tests directs
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
-- Appliquer les privilèges
FLUSH PRIVILEGES;
-- Vérifier la création
SELECT user, host FROM mysql.user WHERE user = 'adresses_user' ORDER BY host;

0
api/scripts/cron/sync_databases.php Normal file → Executable file
View File

View File

@@ -0,0 +1,37 @@
-- Script pour diagnostiquer et corriger les problèmes d'index spatial
-- 1. Vérifier s'il y a des géométries vides
SELECT 'Géométries vides:' as diagnostic;
SELECT code, libelle
FROM x_departements
WHERE contour IS NOT NULL AND ST_IsEmpty(contour) = 1;
-- 2. Essayer de créer un index spatial sur une copie de la table pour tester
CREATE TABLE x_departements_test LIKE x_departements;
-- 3. Copier uniquement les départements métropolitains avec contours
INSERT INTO x_departements_test
SELECT * FROM x_departements
WHERE contour IS NOT NULL
AND code NOT IN ('20', '971', '972', '973', '974', '975', '976');
-- 4. Tenter de créer l'index spatial sur la table de test
ALTER TABLE x_departements_test ADD SPATIAL INDEX idx_contour_test (contour);
-- Si ça fonctionne, le problème vient des départements spécifiques
-- Si ça ne fonctionne pas, il y a un problème avec les données géométriques
-- 5. Alternative : recréer les géométries à partir du texte WKT
-- Cela peut corriger certains problèmes de format
UPDATE x_departements d
INNER JOIN x_departements_contours dc ON d.code = dc.code_dept
SET d.contour = ST_GeomFromText(ST_AsText(dc.contour))
WHERE dc.contour IS NOT NULL
AND d.code IN (SELECT code FROM x_departements WHERE contour IS NOT NULL LIMIT 1);
-- 6. Nettoyer
DROP TABLE IF EXISTS x_departements_test;
-- Note : Pour l'instant, l'index normal créé avec contour(32) permettra
-- le fonctionnement de l'API, même si les performances seront moindres
-- qu'avec un vrai index spatial.

0
api/scripts/geosector.sql Normal file → Executable file
View File

0
api/scripts/geosector_app.sql Normal file → Executable file
View File

View File

@@ -0,0 +1,263 @@
<?php
/**
* Script d'import des contours départementaux depuis un fichier GeoJSON local
*/
class DepartementContoursFileImporter {
private PDO $db;
private array $log = [];
public function __construct(PDO $db) {
$this->db = $db;
}
/**
* Vérifie si la table existe
*/
public function tableExists(): bool {
try {
$sql = "SHOW TABLES LIKE 'x_departements_contours'";
$stmt = $this->db->query($sql);
return $stmt->rowCount() > 0;
} catch (Exception $e) {
return false;
}
}
/**
* Vérifie si la table est vide
*/
private function isTableEmpty(): bool {
try {
$sql = "SELECT COUNT(*) as count FROM x_departements_contours";
$stmt = $this->db->query($sql);
$result = $stmt->fetch();
return $result['count'] == 0;
} catch (Exception $e) {
return true;
}
}
/**
* Importe les départements depuis un fichier GeoJSON
*/
public function importFromFile(string $filePath): array {
$this->log[] = "Début de l'import depuis le fichier : $filePath";
$this->log[] = "";
// Vérifier que le fichier existe
if (!file_exists($filePath)) {
$this->log[] = "✗ Fichier non trouvé : $filePath";
return $this->log;
}
// Vérifier que la table existe
if (!$this->tableExists()) {
$this->log[] = "✗ La table x_departements_contours n'existe pas";
return $this->log;
}
// Vérifier que la table est vide
if (!$this->isTableEmpty()) {
$this->log[] = "✗ La table x_departements_contours contient déjà des données";
return $this->log;
}
// Lire le fichier GeoJSON
$this->log[] = "Lecture du fichier GeoJSON...";
$jsonContent = file_get_contents($filePath);
if ($jsonContent === false) {
$this->log[] = "✗ Impossible de lire le fichier";
return $this->log;
}
// Parser le JSON
$geojson = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
$this->log[] = "✗ Erreur JSON : " . json_last_error_msg();
return $this->log;
}
if (!isset($geojson['features']) || !is_array($geojson['features'])) {
$this->log[] = "✗ Format GeoJSON invalide : pas de features";
return $this->log;
}
$this->log[] = "✓ Fichier chargé : " . count($geojson['features']) . " départements trouvés";
$this->log[] = "";
// Préparer la requête d'insertion
$sql = "INSERT INTO x_departements_contours
(code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng)
VALUES
(:code, :nom, ST_GeomFromText(:polygon, 4326), :min_lat, :max_lat, :min_lng, :max_lng)";
$stmt = $this->db->prepare($sql);
$success = 0;
$errors = 0;
// Démarrer une transaction
$this->db->beginTransaction();
try {
foreach ($geojson['features'] as $feature) {
// Extraire les informations
$code = $feature['properties']['code'] ?? null;
$nom = $feature['properties']['nom'] ?? null;
$geometry = $feature['geometry'] ?? null;
if (!$code || !$nom || !$geometry) {
$this->log[] = "✗ Données manquantes pour un département";
$errors++;
continue;
}
// Convertir la géométrie en WKT
$wktData = $this->geometryToWkt($geometry);
if (!$wktData) {
$this->log[] = "✗ Conversion échouée pour $code ($nom)";
$errors++;
continue;
}
try {
$stmt->execute([
'code' => $code,
'nom' => $nom,
'polygon' => $wktData['wkt'],
'min_lat' => $wktData['bbox']['min_lat'],
'max_lat' => $wktData['bbox']['max_lat'],
'min_lng' => $wktData['bbox']['min_lng'],
'max_lng' => $wktData['bbox']['max_lng']
]);
$this->log[] = "$code - $nom importé";
$success++;
} catch (Exception $e) {
$this->log[] = "✗ Erreur SQL pour $code ($nom) : " . $e->getMessage();
$errors++;
}
}
// Valider ou annuler la transaction
if ($success > 0) {
$this->db->commit();
$this->log[] = "";
$this->log[] = "✓ Transaction validée";
} else {
$this->db->rollBack();
$this->log[] = "";
$this->log[] = "✗ Transaction annulée (aucun import réussi)";
}
} catch (Exception $e) {
$this->db->rollBack();
$this->log[] = "";
$this->log[] = "✗ Erreur fatale : " . $e->getMessage();
$this->log[] = "✗ Transaction annulée";
}
$this->log[] = "";
$this->log[] = "Import terminé : $success réussis, $errors erreurs";
return $this->log;
}
/**
* Convertit une géométrie GeoJSON en WKT
*/
private function geometryToWkt(array $geometry): ?array {
$type = $geometry['type'] ?? null;
$coordinates = $geometry['coordinates'] ?? null;
if (!$type || !$coordinates) {
return null;
}
$wkt = null;
$allPoints = [];
switch ($type) {
case 'Polygon':
// Un seul polygone
$ring = $coordinates[0]; // Anneau extérieur
$points = [];
foreach ($ring as $point) {
$points[] = $point[0] . ' ' . $point[1];
$allPoints[] = $point;
}
$wkt = 'POLYGON((' . implode(',', $points) . '))';
break;
case 'MultiPolygon':
// Plusieurs polygones
$polygons = [];
foreach ($coordinates as $polygon) {
$ring = $polygon[0]; // Anneau extérieur du polygone
$points = [];
foreach ($ring as $point) {
$points[] = $point[0] . ' ' . $point[1];
$allPoints[] = $point;
}
$polygons[] = '((' . implode(',', $points) . '))';
}
$wkt = 'MULTIPOLYGON(' . implode(',', $polygons) . ')';
break;
default:
return null;
}
if (!$wkt || empty($allPoints)) {
return null;
}
// Calculer la bounding box
$lats = array_map(function($p) { return $p[1]; }, $allPoints);
$lngs = array_map(function($p) { return $p[0]; }, $allPoints);
return [
'wkt' => $wkt,
'bbox' => [
'min_lat' => min($lats),
'max_lat' => max($lats),
'min_lng' => min($lngs),
'max_lng' => max($lngs)
]
];
}
}
// Si le script est exécuté directement
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($_SERVER['PHP_SELF'] ?? __FILE__)) {
require_once __DIR__ . '/../src/Config/AppConfig.php';
require_once __DIR__ . '/../src/Core/Database.php';
// Chemin vers le fichier GeoJSON
$filePath = __DIR__ . '/../docs/contour-des-departements.geojson';
// Vérifier les arguments
if ($argc > 1) {
$filePath = $argv[1];
}
echo "Import des contours départementaux depuis un fichier\n";
echo "==================================================\n\n";
echo "Fichier : $filePath\n\n";
$appConfig = AppConfig::getInstance();
Database::init($appConfig->getDatabaseConfig());
$db = Database::getInstance();
$importer = new DepartementContoursFileImporter($db);
$log = $importer->importFromFile($filePath);
foreach ($log as $line) {
echo $line . "\n";
}
}

View File

@@ -0,0 +1,175 @@
<?php
/**
* Script d'import des contours des départements français
*
* Les données peuvent provenir de :
* - IGN Admin Express : https://geoservices.ign.fr/adminexpress
* - data.gouv.fr : https://www.data.gouv.fr/fr/datasets/contours-des-departements-francais-issus-d-openstreetmap/
* - OpenStreetMap via Overpass API
*
* Format attendu : GeoJSON ou Shapefile converti en SQL
*/
require_once __DIR__ . '/../src/Config/AppConfig.php';
require_once __DIR__ . '/../src/Core/Database.php';
echo "Import des contours des départements\n";
echo "===================================\n\n";
// Initialiser la base de données
$appConfig = AppConfig::getInstance();
Database::init($appConfig->getDatabaseConfig());
$db = Database::getInstance();
// Exemple de données pour quelques départements bretons
// En production, ces données viendraient d'un fichier GeoJSON ou d'une API
$departements = [
[
'code' => '22',
'nom' => 'Côtes-d\'Armor',
// Contour simplifié - en réalité il faudrait des centaines de points
'points' => [
[-3.6546, 48.9012], [-3.3856, 48.8756], [-3.1234, 48.8234],
[-2.7856, 48.7845], [-2.4567, 48.7234], [-2.1234, 48.6456],
[-2.0123, 48.5234], [-2.0456, 48.3456], [-2.1567, 48.1234],
[-2.3456, 48.0567], [-2.6789, 48.0789], [-3.0123, 48.1234],
[-3.3456, 48.2345], [-3.5678, 48.4567], [-3.6234, 48.6789],
[-3.6546, 48.9012] // Fermer le polygone
]
],
[
'code' => '29',
'nom' => 'Finistère',
'points' => [
[-5.1423, 48.7523], [-4.8234, 48.6845], [-4.5123, 48.6234],
[-4.2345, 48.5678], [-3.9876, 48.4567], [-3.7234, 48.3456],
[-3.4567, 48.2345], [-3.3876, 48.0123], [-3.4234, 47.8234],
[-3.5678, 47.6456], [-3.8765, 47.6789], [-4.2345, 47.7234],
[-4.5678, 47.8234], [-4.8765, 47.9345], [-5.0876, 48.1234],
[-5.1234, 48.3456], [-5.1345, 48.5678], [-5.1423, 48.7523]
]
],
[
'code' => '35',
'nom' => 'Ille-et-Vilaine',
'points' => [
[-2.0123, 48.6456], [-1.7234, 48.5678], [-1.4567, 48.4567],
[-1.2345, 48.3456], [-1.0234, 48.2345], [-1.0567, 48.0123],
[-1.1234, 47.8234], [-1.2567, 47.6456], [-1.4678, 47.6789],
[-1.7234, 47.7234], [-1.9876, 47.8234], [-2.1234, 47.9345],
[-2.2345, 48.1234], [-2.1567, 48.3456], [-2.0678, 48.5234],
[-2.0123, 48.6456]
]
],
[
'code' => '56',
'nom' => 'Morbihan',
'points' => [
[-3.4567, 48.2345], [-3.2345, 48.1234], [-2.9876, 48.0123],
[-2.7234, 47.9234], [-2.4567, 47.8345], [-2.2345, 47.7456],
[-2.1234, 47.6234], [-2.2567, 47.4567], [-2.4678, 47.3456],
[-2.7234, 47.3789], [-3.0123, 47.4234], [-3.2876, 47.5234],
[-3.5234, 47.6345], [-3.6789, 47.7456], [-3.7234, 47.9234],
[-3.6567, 48.0789], [-3.4567, 48.2345]
]
]
];
try {
$db->beginTransaction();
// Préparer la requête d'insertion
$sql = "INSERT INTO departements_contours
(code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng)
VALUES
(:code, :nom, ST_GeomFromText(:polygon, 4326), :min_lat, :max_lat, :min_lng, :max_lng)
ON DUPLICATE KEY UPDATE
nom_dept = VALUES(nom_dept),
contour = VALUES(contour),
bbox_min_lat = VALUES(bbox_min_lat),
bbox_max_lat = VALUES(bbox_max_lat),
bbox_min_lng = VALUES(bbox_min_lng),
bbox_max_lng = VALUES(bbox_max_lng),
updated_at = CURRENT_TIMESTAMP";
$stmt = $db->prepare($sql);
foreach ($departements as $dept) {
echo "Import du département {$dept['code']} - {$dept['nom']}...\n";
// Créer le polygone WKT
$polygonPoints = [];
$lats = [];
$lngs = [];
foreach ($dept['points'] as $point) {
$lng = $point[0];
$lat = $point[1];
$polygonPoints[] = "$lng $lat";
$lats[] = $lat;
$lngs[] = $lng;
}
$polygon = 'POLYGON((' . implode(',', $polygonPoints) . '))';
// Calculer la bounding box
$minLat = min($lats);
$maxLat = max($lats);
$minLng = min($lngs);
$maxLng = max($lngs);
// Exécuter l'insertion
$stmt->execute([
'code' => $dept['code'],
'nom' => $dept['nom'],
'polygon' => $polygon,
'min_lat' => $minLat,
'max_lat' => $maxLat,
'min_lng' => $minLng,
'max_lng' => $maxLng
]);
echo "✓ Département {$dept['code']} importé\n";
}
$db->commit();
echo "\n✓ Import terminé avec succès!\n\n";
// Vérifier les données importées
$checkSql = "SELECT code_dept, nom_dept,
ST_Area(contour) as area,
ST_NumPoints(ST_ExteriorRing(contour)) as num_points
FROM x_departements_contours
ORDER BY code_dept";
$result = $db->query($checkSql);
echo "Départements importés:\n";
echo "---------------------\n";
foreach ($result as $row) {
echo sprintf("- %s (%s) : %d points, aire: %.4f\n",
$row['nom_dept'],
$row['code_dept'],
$row['num_points'],
$row['area']
);
}
} catch (Exception $e) {
$db->rollBack();
echo "✗ Erreur lors de l'import : " . $e->getMessage() . "\n";
}
echo "\n";
echo "Note importante:\n";
echo "---------------\n";
echo "Ce script utilise des données simplifiées pour l'exemple.\n";
echo "Pour un usage en production, vous devez :\n";
echo "1. Télécharger les vrais contours depuis l'IGN ou data.gouv.fr\n";
echo "2. Les convertir en format GeoJSON ou SQL\n";
echo "3. Adapter ce script pour lire ces fichiers\n";
echo "\n";
echo "Sources recommandées:\n";
echo "- IGN Admin Express: https://geoservices.ign.fr/adminexpress\n";
echo "- data.gouv.fr: https://www.data.gouv.fr/fr/datasets/contours-des-departements-francais-issus-d-openstreetmap/\n";

View File

@@ -0,0 +1,318 @@
<?php
/**
* Script d'initialisation des contours des départements français
* À exécuter une seule fois lors de la connexion de l'admin d6soft
*
* Utilise l'API geo.api.gouv.fr pour récupérer les contours GeoJSON
*/
class DepartementContoursInitializer {
private PDO $db;
private array $log = [];
// Liste complète des départements français (métropole + DOM-TOM)
private array $departements = [
// Métropole
'01' => 'Ain', '02' => 'Aisne', '03' => 'Allier', '04' => 'Alpes-de-Haute-Provence',
'05' => 'Hautes-Alpes', '06' => 'Alpes-Maritimes', '07' => 'Ardèche', '08' => 'Ardennes',
'09' => 'Ariège', '10' => 'Aube', '11' => 'Aude', '12' => 'Aveyron',
'13' => 'Bouches-du-Rhône', '14' => 'Calvados', '15' => 'Cantal', '16' => 'Charente',
'17' => 'Charente-Maritime', '18' => 'Cher', '19' => 'Corrèze', '2A' => 'Corse-du-Sud',
'2B' => 'Haute-Corse', '21' => 'Côte-d\'Or', '22' => 'Côtes-d\'Armor', '23' => 'Creuse',
'24' => 'Dordogne', '25' => 'Doubs', '26' => 'Drôme', '27' => 'Eure',
'28' => 'Eure-et-Loir', '29' => 'Finistère', '30' => 'Gard', '31' => 'Haute-Garonne',
'32' => 'Gers', '33' => 'Gironde', '34' => 'Hérault', '35' => 'Ille-et-Vilaine',
'36' => 'Indre', '37' => 'Indre-et-Loire', '38' => 'Isère', '39' => 'Jura',
'40' => 'Landes', '41' => 'Loir-et-Cher', '42' => 'Loire', '43' => 'Haute-Loire',
'44' => 'Loire-Atlantique', '45' => 'Loiret', '46' => 'Lot', '47' => 'Lot-et-Garonne',
'48' => 'Lozère', '49' => 'Maine-et-Loire', '50' => 'Manche', '51' => 'Marne',
'52' => 'Haute-Marne', '53' => 'Mayenne', '54' => 'Meurthe-et-Moselle', '55' => 'Meuse',
'56' => 'Morbihan', '57' => 'Moselle', '58' => 'Nièvre', '59' => 'Nord',
'60' => 'Oise', '61' => 'Orne', '62' => 'Pas-de-Calais', '63' => 'Puy-de-Dôme',
'64' => 'Pyrénées-Atlantiques', '65' => 'Hautes-Pyrénées', '66' => 'Pyrénées-Orientales', '67' => 'Bas-Rhin',
'68' => 'Haut-Rhin', '69' => 'Rhône', '70' => 'Haute-Saône', '71' => 'Saône-et-Loire',
'72' => 'Sarthe', '73' => 'Savoie', '74' => 'Haute-Savoie', '75' => 'Paris',
'76' => 'Seine-Maritime', '77' => 'Seine-et-Marne', '78' => 'Yvelines', '79' => 'Deux-Sèvres',
'80' => 'Somme', '81' => 'Tarn', '82' => 'Tarn-et-Garonne', '83' => 'Var',
'84' => 'Vaucluse', '85' => 'Vendée', '86' => 'Vienne', '87' => 'Haute-Vienne',
'88' => 'Vosges', '89' => 'Yonne', '90' => 'Territoire de Belfort', '91' => 'Essonne',
'92' => 'Hauts-de-Seine', '93' => 'Seine-Saint-Denis', '94' => 'Val-de-Marne', '95' => 'Val-d\'Oise',
// DOM-TOM
'971' => 'Guadeloupe', '972' => 'Martinique', '973' => 'Guyane', '974' => 'La Réunion',
'975' => 'Saint-Pierre-et-Miquelon', '976' => 'Mayotte', '977' => 'Saint-Barthélemy',
'978' => 'Saint-Martin', '984' => 'Terres australes et antarctiques françaises',
'986' => 'Wallis-et-Futuna', '987' => 'Polynésie française', '988' => 'Nouvelle-Calédonie'
];
public function __construct(PDO $db) {
$this->db = $db;
}
/**
* Vérifie si la table existe
*/
public function tableExists(): bool {
try {
$sql = "SHOW TABLES LIKE 'x_departements_contours'";
$stmt = $this->db->query($sql);
return $stmt->rowCount() > 0;
} catch (Exception $e) {
return false;
}
}
/**
* Vérifie si la table est vide
*/
private function isTableEmpty(): bool {
try {
$sql = "SELECT COUNT(*) as count FROM x_departements_contours";
$stmt = $this->db->query($sql);
$result = $stmt->fetch();
return $result['count'] == 0;
} catch (Exception $e) {
return true;
}
}
/**
* Récupère le contour d'un département depuis l'API geo.api.gouv.fr
*/
private function fetchDepartementContour(string $code, string $nom): ?array {
// URL de l'API pour récupérer le contour du département en GeoJSON
$url = "https://geo.api.gouv.fr/departements/{$code}?geometry=contour";
$context = stream_context_create([
'http' => [
'timeout' => 30,
'header' => "User-Agent: Geosector/1.0\r\n"
]
]);
$response = @file_get_contents($url, false, $context);
if ($response === false) {
$this->log[] = "✗ Erreur API pour département $code ($nom)";
return null;
}
$data = json_decode($response, true);
// L'API peut retourner le contour dans 'contour' ou 'geometry'
if (isset($data['contour']) && isset($data['contour']['coordinates'])) {
return $data['contour'];
} elseif (isset($data['geometry']) && isset($data['geometry']['coordinates'])) {
return $data['geometry'];
} else {
$this->log[] = "✗ Pas de contour pour département $code ($nom)";
// Debug : afficher les clés disponibles
if (is_array($data)) {
$this->log[] = " Clés disponibles : " . implode(', ', array_keys($data));
}
return null;
}
}
/**
* Convertit les coordonnées GeoJSON en WKT Polygon pour MySQL
*/
private function geoJsonToWkt(array $coordinates): ?array {
if (empty($coordinates) || !is_array($coordinates[0])) {
return null;
}
// GeoJSON peut avoir plusieurs niveaux d'imbrication selon le type
// Pour un Polygon simple
if (isset($coordinates[0][0]) && is_numeric($coordinates[0][0])) {
$ring = $coordinates;
}
// Pour un MultiPolygon, prendre le premier polygone
elseif (isset($coordinates[0][0][0])) {
$ring = $coordinates[0][0];
}
// Pour un Polygon standard
else {
$ring = $coordinates[0];
}
$points = [];
$lats = [];
$lngs = [];
foreach ($ring as $point) {
if (count($point) >= 2) {
$lng = $point[0];
$lat = $point[1];
$points[] = "$lng $lat";
$lats[] = $lat;
$lngs[] = $lng;
}
}
if (count($points) < 3) {
return null;
}
// Fermer le polygone si nécessaire
if ($points[0] !== $points[count($points) - 1]) {
$points[] = $points[0];
}
return [
'wkt' => 'POLYGON((' . implode(',', $points) . '))',
'bbox' => [
'min_lat' => min($lats),
'max_lat' => max($lats),
'min_lng' => min($lngs),
'max_lng' => max($lngs)
]
];
}
/**
* Importe tous les départements
*/
public function importAll(): array {
$this->log[] = "Début de l'import des contours départementaux";
$this->log[] = "Source : API geo.api.gouv.fr";
$this->log[] = "";
// Vérifier que la table est vide avant d'importer
if (!$this->isTableEmpty()) {
$this->log[] = "✗ La table x_departements_contours contient déjà des données";
return $this->log;
}
// Préparer la requête d'insertion
$sql = "INSERT INTO x_departements_contours
(code_dept, nom_dept, contour, bbox_min_lat, bbox_max_lat, bbox_min_lng, bbox_max_lng)
VALUES
(:code, :nom, ST_GeomFromText(:polygon, 4326), :min_lat, :max_lat, :min_lng, :max_lng)";
$stmt = $this->db->prepare($sql);
$success = 0;
$errors = 0;
// Démarrer une transaction
$this->db->beginTransaction();
try {
foreach ($this->departements as $code => $nom) {
// Petite pause pour ne pas surcharger l'API
usleep(100000); // 100ms
$contour = $this->fetchDepartementContour($code, $nom);
if (!$contour) {
$errors++;
continue;
}
$wktData = $this->geoJsonToWkt($contour['coordinates']);
if (!$wktData) {
$this->log[] = "✗ Conversion échouée pour $code ($nom)";
$errors++;
continue;
}
try {
$stmt->execute([
'code' => $code,
'nom' => $nom,
'polygon' => $wktData['wkt'],
'min_lat' => $wktData['bbox']['min_lat'],
'max_lat' => $wktData['bbox']['max_lat'],
'min_lng' => $wktData['bbox']['min_lng'],
'max_lng' => $wktData['bbox']['max_lng']
]);
$this->log[] = "$code - $nom importé";
$success++;
} catch (Exception $e) {
$this->log[] = "✗ Erreur SQL pour $code ($nom) : " . $e->getMessage();
$errors++;
}
}
// Si tout s'est bien passé, valider la transaction
if ($success > 0) {
$this->db->commit();
$this->log[] = "";
$this->log[] = "✓ Transaction validée";
} else {
$this->db->rollBack();
$this->log[] = "";
$this->log[] = "✗ Transaction annulée (aucun import réussi)";
}
} catch (Exception $e) {
$this->db->rollBack();
$this->log[] = "";
$this->log[] = "✗ Erreur fatale : " . $e->getMessage();
$this->log[] = "✗ Transaction annulée";
$errors = count($this->departements);
}
$this->log[] = "";
$this->log[] = "Import terminé : $success réussis, $errors erreurs";
return $this->log;
}
/**
* Exécute l'initialisation si nécessaire
*/
public static function runIfNeeded(PDO $db, string $username): ?array {
// Vérifier que c'est bien l'admin d6soft
if ($username !== 'd6soft') {
return null;
}
$initializer = new self($db);
// Vérifier si la table existe
if (!$initializer->tableExists()) {
return ["✗ La table x_departements_contours n'existe pas. Veuillez la créer avec le script SQL fourni."];
}
// Vérifier si elle est vide
if (!$initializer->isTableEmpty()) {
return null; // Table déjà remplie, rien à faire
}
// Vérifier si le fichier local existe
$localFile = __DIR__ . '/../docs/contour-des-departements.geojson';
if (file_exists($localFile)) {
// Utiliser le fichier local
require_once __DIR__ . '/import_departements_from_file.php';
$fileImporter = new \DepartementContoursFileImporter($db);
return $fileImporter->importFromFile($localFile);
}
// Sinon, utiliser l'API (qui ne fonctionne pas bien actuellement)
return $initializer->importAll();
}
}
// Si le script est exécuté directement (pour tests)
if (php_sapi_name() === 'cli' && basename(__FILE__) === basename($_SERVER['PHP_SELF'] ?? __FILE__)) {
require_once __DIR__ . '/../src/Config/AppConfig.php';
require_once __DIR__ . '/../src/Core/Database.php';
$appConfig = AppConfig::getInstance();
Database::init($appConfig->getDatabaseConfig());
$db = Database::getInstance();
echo "Test d'import des contours départementaux\n";
echo "========================================\n\n";
$initializer = new DepartementContoursInitializer($db);
$log = $initializer->importAll();
foreach ($log as $line) {
echo $line . "\n";
}
}

0
api/scripts/php/MigrationConfig.php Normal file → Executable file
View File

0
api/scripts/php/migrate.php Normal file → Executable file
View File

0
api/scripts/php/migrate_entites.php Normal file → Executable file
View File

0
api/scripts/php/migrate_medias.php Normal file → Executable file
View File

0
api/scripts/php/migrate_ope_pass.php Normal file → Executable file
View File

0
api/scripts/php/migrate_ope_pass_histo.php Normal file → Executable file
View File

0
api/scripts/php/migrate_ope_sectors.php Normal file → Executable file
View File

0
api/scripts/php/migrate_ope_users.php Normal file → Executable file
View File

0
api/scripts/php/migrate_ope_users_sectors.php Normal file → Executable file
View File

0
api/scripts/php/migrate_operations.php Normal file → Executable file
View File

0
api/scripts/php/migrate_sectors_adresses.php Normal file → Executable file
View File

0
api/scripts/php/migrate_users.php Normal file → Executable file
View File

0
api/scripts/php/migrate_x_departements.php Normal file → Executable file
View File

0
api/scripts/php/migrate_x_devises.php Normal file → Executable file
View File

0
api/scripts/php/migrate_x_entites_types.php Normal file → Executable file
View File

0
api/scripts/php/migrate_x_pays.php Normal file → Executable file
View File

0
api/scripts/php/migrate_x_regions.php Normal file → Executable file
View File

0
api/scripts/php/migrate_x_types_passages.php Normal file → Executable file
View File

0
api/scripts/php/migrate_x_types_reglements.php Normal file → Executable file
View File

0
api/scripts/php/migrate_x_users_roles.php Normal file → Executable file
View File

0
api/scripts/php/migrate_x_villes.php Normal file → Executable file
View File

View File

@@ -0,0 +1,106 @@
#!/bin/bash
# Script pour configurer l'accès à la base de données des adresses
# sur chaque environnement Incus
echo "Configuration de l'accès à la base de données des adresses"
echo "=========================================================="
# Fonction pour créer l'utilisateur sur un container
create_user_on_container() {
local container=$1
local api_ip=$2
local maria_ip=$3
echo ""
echo "Configuration pour $container..."
echo "Container MariaDB IP: $maria_ip"
echo "Container API IP: $api_ip"
# Se connecter au container MariaDB et créer l'utilisateur
incus exec $container -- mysql -u root -p -e "
-- Créer l'utilisateur pour l'accès depuis l'API
CREATE USER IF NOT EXISTS 'adresses_user'@'$api_ip' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'$api_ip';
-- Créer aussi un utilisateur localhost pour les tests directs
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
-- Optionnel : créer un utilisateur pour tout le sous-réseau
CREATE USER IF NOT EXISTS 'adresses_user'@'13.23.33.%' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'13.23.33.%';
FLUSH PRIVILEGES;
-- Vérifier
SELECT user, host FROM mysql.user WHERE user = 'adresses_user';
"
echo "✓ Utilisateur créé sur $container"
}
# Fonction pour tester la connexion
test_connection() {
local api_container=$1
local maria_ip=$2
echo ""
echo "Test de connexion depuis $api_container vers $maria_ip..."
incus exec $api_container -- mysql -h $maria_ip -u adresses_user -p'd66,AdrGeo.User' -e "
SELECT DATABASE();
SHOW TABLES FROM adresses LIMIT 5;
"
if [ $? -eq 0 ]; then
echo "✓ Connexion réussie!"
else
echo "✗ Échec de la connexion"
fi
}
# Configuration pour chaque environnement
echo ""
echo "1. DÉVELOPPEMENT (DVA)"
read -p "IP du container dva-api [par défaut: 13.23.33.45]: " DVA_API_IP
DVA_API_IP=${DVA_API_IP:-13.23.33.45}
create_user_on_container "dva-maria" "$DVA_API_IP" "13.23.33.46"
echo ""
echo "2. RECETTE (RCA)"
read -p "IP du container rca-api [par défaut: 13.23.33.35]: " RCA_API_IP
RCA_API_IP=${RCA_API_IP:-13.23.33.35}
create_user_on_container "rca-maria" "$RCA_API_IP" "13.23.33.36"
echo ""
echo "3. PRODUCTION (PRA)"
read -p "IP du container pra-api [par défaut: 13.23.33.25]: " PRA_API_IP
PRA_API_IP=${PRA_API_IP:-13.23.33.25}
create_user_on_container "pra-maria" "$PRA_API_IP" "13.23.33.26"
# Tests de connexion
echo ""
echo "=========================================================="
echo "Tests de connexion"
echo "=========================================================="
read -p "Voulez-vous tester les connexions? (o/n): " test_choice
if [ "$test_choice" = "o" ]; then
test_connection "dva-api" "13.23.33.46"
test_connection "rca-api" "13.23.33.36"
test_connection "pra-api" "13.23.33.26"
fi
echo ""
echo "Configuration terminée!"
echo ""
echo "Notes importantes:"
echo "- Les utilisateurs ont été créés avec accès SELECT uniquement sur la base 'adresses'"
echo "- Trois types d'accès ont été configurés:"
echo " 1. Depuis l'IP spécifique de chaque container API"
echo " 2. Depuis localhost (pour les tests directs)"
echo " 3. Depuis tout le sous-réseau 13.23.33.% (optionnel, moins sécurisé)"
echo ""
echo "Pour tester manuellement depuis un container API:"
echo "incus exec [container-api] -- mysql -h [ip-maria] -u adresses_user -p'd66,AdrGeo.User' adresses"

View File

@@ -0,0 +1,136 @@
#!/bin/bash
# Script pour configurer l'accès à la base de données des adresses
# avec segmentation par environnement basée sur les plages d'IPs
echo "Configuration de l'accès à la base de données des adresses"
echo "=========================================================="
echo ""
echo "Architecture des IPs par environnement :"
echo "- DÉVELOPPEMENT : 13.23.33.40-49 (13.23.33.4%)"
echo "- RECETTE : 13.23.33.30-39 (13.23.33.3%)"
echo "- PRODUCTION : 13.23.33.20-29 (13.23.33.2%)"
echo ""
# Fonction pour créer l'utilisateur sur un container
create_user_on_container() {
local container=$1
local ip_pattern=$2
local env_name=$3
echo ""
echo "Configuration pour $env_name ($container)..."
echo "Pattern IP autorisé : $ip_pattern"
# Se connecter au container MariaDB et créer l'utilisateur
incus exec $container -- mysql -u root -p -e "
-- Créer l'utilisateur pour l'accès depuis la plage IP de l'environnement
CREATE USER IF NOT EXISTS 'adresses_user'@'$ip_pattern' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'$ip_pattern';
-- Créer aussi un utilisateur localhost pour les tests directs
CREATE USER IF NOT EXISTS 'adresses_user'@'localhost' IDENTIFIED BY 'd66,AdrGeo.User';
GRANT SELECT ON adresses.* TO 'adresses_user'@'localhost';
FLUSH PRIVILEGES;
-- Vérifier
SELECT user, host FROM mysql.user WHERE user = 'adresses_user' ORDER BY host;
"
if [ $? -eq 0 ]; then
echo "✓ Utilisateur créé sur $container"
else
echo "✗ Erreur lors de la création sur $container"
fi
}
# Fonction pour tester la connexion
test_connection() {
local api_container=$1
local maria_ip=$2
local env_name=$3
echo ""
echo "Test de connexion $env_name : $api_container -> $maria_ip..."
incus exec $api_container -- mysql -h $maria_ip -u adresses_user -p'd66,AdrGeo.User' -e "
SELECT CONCAT('Connexion réussie depuis ', @@hostname, ' vers ', '$maria_ip') as Status;
SELECT DATABASE();
SHOW TABLES FROM adresses LIMIT 3;
" 2>/dev/null
if [ $? -eq 0 ]; then
echo "✓ Connexion réussie!"
else
echo "✗ Échec de la connexion"
fi
}
# Menu de sélection
echo ""
echo "Que voulez-vous configurer ?"
echo "1. Environnement DÉVELOPPEMENT uniquement (dva-maria)"
echo "2. Environnement RECETTE uniquement (rca-maria)"
echo "3. Environnement PRODUCTION uniquement (pra-maria)"
echo "4. Tous les environnements"
echo ""
read -p "Votre choix (1-4): " choice
case $choice in
1)
create_user_on_container "dva-maria" "13.23.33.4%" "DÉVELOPPEMENT"
;;
2)
create_user_on_container "rca-maria" "13.23.33.3%" "RECETTE"
;;
3)
create_user_on_container "pra-maria" "13.23.33.2%" "PRODUCTION"
;;
4)
create_user_on_container "dva-maria" "13.23.33.4%" "DÉVELOPPEMENT"
create_user_on_container "rca-maria" "13.23.33.3%" "RECETTE"
create_user_on_container "pra-maria" "13.23.33.2%" "PRODUCTION"
;;
*)
echo "Choix invalide"
exit 1
;;
esac
# Tests de connexion
echo ""
echo "=========================================================="
echo "Tests de connexion"
echo "=========================================================="
read -p "Voulez-vous tester les connexions? (o/n): " test_choice
if [ "$test_choice" = "o" ]; then
case $choice in
1)
test_connection "dva-api" "13.23.33.46" "DÉVELOPPEMENT"
;;
2)
test_connection "rca-api" "13.23.33.36" "RECETTE"
;;
3)
test_connection "pra-api" "13.23.33.26" "PRODUCTION"
;;
4)
test_connection "dva-api" "13.23.33.46" "DÉVELOPPEMENT"
test_connection "rca-api" "13.23.33.36" "RECETTE"
test_connection "pra-api" "13.23.33.26" "PRODUCTION"
;;
esac
fi
echo ""
echo "Configuration terminée!"
echo ""
echo "Récapitulatif de la sécurité mise en place :"
echo "- Chaque environnement a sa propre plage d'IPs autorisée"
echo "- DÉVELOPPEMENT : seuls les containers en 13.23.33.4x peuvent accéder à dva-maria"
echo "- RECETTE : seuls les containers en 13.23.33.3x peuvent accéder à rca-maria"
echo "- PRODUCTION : seuls les containers en 13.23.33.2x peuvent accéder à pra-maria"
echo ""
echo "Cela empêche un container de DEV d'accéder aux données de PROD par exemple."

27
api/src/Config/AppConfig.php Normal file → Executable file
View File

@@ -88,6 +88,12 @@ class AppConfig {
'username' => 'geo_app_user_prod', 'username' => 'geo_app_user_prod',
'password' => 'QO:96-SrHJ6k7-df*?k{4W6m', 'password' => 'QO:96-SrHJ6k7-df*?k{4W6m',
], ],
'addresses_database' => [
'host' => '13.23.33.26',
'name' => 'adresses',
'username' => 'adr_geo_user',
'password' => 'd66,AdrGeo.User',
],
]); ]);
// Configuration RECETTE // Configuration RECETTE
@@ -99,6 +105,12 @@ class AppConfig {
'username' => 'geo_app_user_rec', 'username' => 'geo_app_user_rec',
'password' => 'QO:96df*?k-dS3KiO-{4W6m', 'password' => 'QO:96df*?k-dS3KiO-{4W6m',
], ],
'addresses_database' => [
'host' => '13.23.33.36',
'name' => 'adresses',
'username' => 'adr_geo_user',
'password' => 'd66,AdrGeoRec.User',
],
// Vous pouvez remplacer d'autres paramètres spécifiques à l'environnement de recette ici // Vous pouvez remplacer d'autres paramètres spécifiques à l'environnement de recette ici
]); ]);
@@ -111,6 +123,12 @@ class AppConfig {
'username' => 'geo_app_user_dev', 'username' => 'geo_app_user_dev',
'password' => '34GOz-X5gJu-oH@Fa3$#Z', 'password' => '34GOz-X5gJu-oH@Fa3$#Z',
], ],
'addresses_database' => [
'host' => '13.23.33.46',
'name' => 'adresses',
'username' => 'adr_geo_user',
'password' => 'd66,AdrGeoDev.User',
],
// Vous pouvez activer des fonctionnalités de débogage en développement // Vous pouvez activer des fonctionnalités de débogage en développement
'debug' => true, 'debug' => true,
// Configurez des endpoints de test pour Stripe, etc. // Configurez des endpoints de test pour Stripe, etc.
@@ -234,6 +252,15 @@ class AppConfig {
return $this->getCurrentConfig()['database']; return $this->getCurrentConfig()['database'];
} }
/**
* Retourne la configuration de la base de données des adresses
*
* @return array Configuration de la base de données des adresses
*/
public function getAddressesDatabaseConfig(): array {
return $this->getCurrentConfig()['addresses_database'];
}
/** /**
* Retourne la clé de chiffrement * Retourne la clé de chiffrement
* *

0
api/src/Controllers/EntiteController.php Normal file → Executable file
View File

0
api/src/Controllers/FileController.php Normal file → Executable file
View File

0
api/src/Controllers/LogController.php Normal file → Executable file
View File

30
api/src/Controllers/LoginController.php Normal file → Executable file
View File

@@ -49,8 +49,8 @@ class LoginController {
// Récupérer le type d'utilisateur // Récupérer le type d'utilisateur
// admin accessible uniquement aux fk_role>1 // admin accessible uniquement aux fk_role>1
// sinon tout user peut se connecter à l'interface utilisateur // user accessible uniquement aux fk_role=1
$roleCondition = ($interface === 'user') ? '' : 'AND fk_role>1'; $roleCondition = ($interface === 'user') ? 'AND fk_role=1' : 'AND fk_role>1';
// Log pour le debug // Log pour le debug
LogService::log('Tentative de connexion GeoSector', [ LogService::log('Tentative de connexion GeoSector', [
@@ -160,6 +160,32 @@ class LoginController {
]; ];
Session::login($sessionData); Session::login($sessionData);
// Vérifier et exécuter l'initialisation des contours départementaux pour d6soft
if ($username === 'd6soft') {
require_once __DIR__ . '/../../scripts/init_departements_contours.php';
$initLog = \DepartementContoursInitializer::runIfNeeded($this->db, $username);
if ($initLog !== null) {
// Logger l'initialisation
LogService::log('Initialisation des contours départementaux', [
'level' => 'info',
'username' => $username,
'log_count' => count($initLog)
]);
// Logger aussi les dernières lignes du log pour diagnostic
$lastLines = array_slice($initLog, -5);
foreach ($lastLines as $line) {
if (strpos($line, '✗') !== false || strpos($line, 'terminé') !== false) {
LogService::log('Import contours: ' . $line, [
'level' => 'info',
'username' => $username
]);
}
}
}
}
// Préparation des données utilisateur pour la réponse (uniquement les champs du user) // Préparation des données utilisateur pour la réponse (uniquement les champs du user)
$userData = [ $userData = [
'id' => $user['id'], 'id' => $user['id'],

0
api/src/Controllers/OperationController.php Normal file → Executable file
View File

0
api/src/Controllers/PassageController.php Normal file → Executable file
View File

File diff suppressed because it is too large Load Diff

162
api/src/Controllers/UserController.php Normal file → Executable file
View File

@@ -550,28 +550,27 @@ class UserController {
// ——— Gestion du transfert éventuel ——— // ——— Gestion du transfert éventuel ———
$transferTo = isset($_GET['transfer_to']) ? trim($_GET['transfer_to']) : null; $transferTo = isset($_GET['transfer_to']) ? trim($_GET['transfer_to']) : null;
$operationId = isset($_GET['operation_id']) ? trim($_GET['operation_id']) : null;
if (($transferTo && !$operationId) || (!$transferTo && $operationId)) { if ($transferTo) {
Response::json([
'status' => 'error',
'message' => "Il faut fournir transfer_to ET operation_id ou aucun des deux"
], 400);
return;
}
if ($transferTo && $operationId) {
try { try {
// Transférer TOUS les passages de l'utilisateur vers l'utilisateur désigné
$stmt3 = $this->db->prepare(' $stmt3 = $this->db->prepare('
UPDATE passages UPDATE ope_pass
SET fk_user = :new_user_id SET fk_user = :new_user_id
WHERE fk_user = :delete_user_id WHERE fk_user = :delete_user_id
AND fk_operation = :operation_id
'); ');
$stmt3->execute([ $stmt3->execute([
'new_user_id' => $transferTo, 'new_user_id' => $transferTo,
'delete_user_id' => $id, 'delete_user_id' => $id
'operation_id' => $operationId ]);
$transferredCount = $stmt3->rowCount();
LogService::log('Passages transférés avant suppression utilisateur', [
'level' => 'info',
'from_user' => $id,
'to_user' => $transferTo,
'passages_transferred' => $transferredCount
]); ]);
} catch (PDOException $e) { } catch (PDOException $e) {
Response::json([ Response::json([
@@ -589,7 +588,9 @@ class UserController {
$stmtOpeUsers = $this->db->prepare('DELETE FROM ope_users WHERE fk_user = ?'); $stmtOpeUsers = $this->db->prepare('DELETE FROM ope_users WHERE fk_user = ?');
$stmtOpeUsers->execute([$id]); $stmtOpeUsers->execute([$id]);
// Ici éventuellement : d'autres suppressions en cascade si besoin // Supprimer les enregistrements dépendants dans ope_users_sectors
$stmtOpeUsersSectors = $this->db->prepare('DELETE FROM ope_users_sectors WHERE fk_user = ?');
$stmtOpeUsersSectors->execute([$id]);
$stmt = $this->db->prepare('DELETE FROM users WHERE id = ?'); $stmt = $this->db->prepare('DELETE FROM users WHERE id = ?');
$stmt->execute([$id]); $stmt->execute([$id]);
@@ -606,7 +607,7 @@ class UserController {
'level' => 'info', 'level' => 'info',
'deletedBy' => $currentUserId, 'deletedBy' => $currentUserId,
'userId' => $id, 'userId' => $id,
'passage_transfer' => $transferTo && $operationId ? "Vers utilisateur $transferTo pour operation $operationId" : 'Aucun' 'passage_transfer' => $transferTo ? "Tous les passages transférés vers utilisateur $transferTo" : 'Aucun transfert'
]); ]);
Response::json([ Response::json([
@@ -626,6 +627,135 @@ class UserController {
} }
} }
public function resetPassword(string $id): void {
Session::requireAuth();
$currentUserId = Session::getUserId();
// Récupérer les infos de l'utilisateur courant
$stmt = $this->db->prepare('SELECT fk_role, fk_entite, chk_active FROM users WHERE id = ?');
$stmt->execute([$currentUserId]);
$currentUser = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$currentUser) {
Response::json([
'status' => 'error',
'message' => 'Utilisateur courant non trouvé'
], 403);
return;
}
// Vérifier que l'utilisateur courant est actif
if ($currentUser['chk_active'] != 1) {
Response::json([
'status' => 'error',
'message' => 'Votre compte n\'est pas actif'
], 403);
return;
}
$userRole = (int)$currentUser['fk_role'];
$userEntite = $currentUser['fk_entite'];
// Récupérer l'utilisateur cible
$stmt = $this->db->prepare('
SELECT id, encrypted_email, encrypted_name, fk_entite, chk_active
FROM users
WHERE id = ?
');
$stmt->execute([$id]);
$targetUser = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$targetUser) {
Response::json([
'status' => 'error',
'message' => 'Utilisateur non trouvé'
], 404);
return;
}
// Vérifier que l'utilisateur est actif
if ($targetUser['chk_active'] != 1) {
Response::json([
'status' => 'error',
'message' => 'L\'utilisateur n\'est pas actif'
], 400);
return;
}
// Contrôle des droits selon le rôle
if ($userRole === 1) {
// Role 1 : peut uniquement réinitialiser son propre mot de passe
if ($currentUserId != $id) {
Response::json([
'status' => 'error',
'message' => 'Vous ne pouvez réinitialiser que votre propre mot de passe'
], 403);
return;
}
} elseif ($userRole === 2) {
// Role 2 : peut réinitialiser les mots de passe de sa propre entité
if ($userEntite != $targetUser['fk_entite']) {
Response::json([
'status' => 'error',
'message' => 'Vous ne pouvez réinitialiser que les mots de passe des utilisateurs de votre entité'
], 403);
return;
}
}
// Role > 2 : peut tout faire
try {
// Déchiffrement des données
$email = ApiService::decryptSearchableData($targetUser['encrypted_email']);
$name = ApiService::decryptData($targetUser['encrypted_name']);
// Génération d'un nouveau mot de passe sécurisé
$newPassword = ApiService::generateSecurePassword();
$passwordHash = password_hash($newPassword, PASSWORD_DEFAULT);
// Mise à jour du mot de passe en base de données
$updateStmt = $this->db->prepare('
UPDATE users
SET user_pass_hash = :password,
updated_at = NOW(),
fk_user_modif = :modifier_id
WHERE id = :id
');
$updateStmt->execute([
'password' => $passwordHash,
'modifier_id' => $currentUserId,
'id' => $id
]);
// Envoi de l'email avec le nouveau mot de passe
ApiService::sendEmail($email, $name, 'password_reset', ['password' => $newPassword]);
LogService::log('Mot de passe réinitialisé', [
'level' => 'info',
'resetBy' => $currentUserId,
'userId' => $id,
'email' => $email
]);
Response::json([
'status' => 'success',
'message' => 'Mot de passe réinitialisé avec succès. Un email a été envoyé à l\'utilisateur.'
]);
} catch (PDOException $e) {
LogService::log('Erreur lors de la réinitialisation du mot de passe', [
'level' => 'error',
'error' => $e->getMessage(),
'userId' => $id
]);
Response::json([
'status' => 'error',
'message' => 'Erreur serveur'
], 500);
}
}
// Méthodes auxiliaires // Méthodes auxiliaires
private function validateUpdateData(array $data): ?string { private function validateUpdateData(array $data): ?string {
// Validation de l'email // Validation de l'email

0
api/src/Controllers/VilleController.php Normal file → Executable file
View File

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
class AddressesDatabase {
private static ?PDO $instance = null;
private static array $config;
public static function init(array $config): void {
self::$config = $config;
}
public static function getInstance(): PDO {
if (self::$instance === null) {
try {
$dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",
self::$config['host'],
self::$config['name']
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
self::$instance = new PDO(
$dsn,
self::$config['username'],
self::$config['password'],
$options
);
} catch (PDOException $e) {
throw new RuntimeException("Addresses database connection failed: " . $e->getMessage());
}
}
return self::$instance;
}
/**
* Ferme la connexion à la base de données des adresses
*/
public static function close(): void {
self::$instance = null;
}
}

0
api/src/Core/Database.php Normal file → Executable file
View File

0
api/src/Core/Request.php Normal file → Executable file
View File

0
api/src/Core/Response.php Normal file → Executable file
View File

7
api/src/Core/Router.php Normal file → Executable file
View File

@@ -37,6 +37,7 @@ class Router {
$this->post('users', ['UserController', 'createUser']); $this->post('users', ['UserController', 'createUser']);
$this->put('users/:id', ['UserController', 'updateUser']); $this->put('users/:id', ['UserController', 'updateUser']);
$this->delete('users/:id', ['UserController', 'deleteUser']); $this->delete('users/:id', ['UserController', 'deleteUser']);
$this->post('users/:id/reset-password', ['UserController', 'resetPassword']);
$this->post('logout', ['LoginController', 'logout']); $this->post('logout', ['LoginController', 'logout']);
// Routes entités // Routes entités
@@ -80,6 +81,12 @@ class Router {
$this->get('files/info/:id', ['FileController', 'getFileInfo']); $this->get('files/info/:id', ['FileController', 'getFileInfo']);
$this->get('files/download/:id', ['FileController', 'download']); $this->get('files/download/:id', ['FileController', 'download']);
$this->delete('files/:id', ['FileController', 'deleteFile']); $this->delete('files/:id', ['FileController', 'deleteFile']);
// Routes secteurs
$this->get('sectors', ['SectorController', 'index']);
$this->post('sectors', ['SectorController', 'create']);
$this->put('sectors/:id', ['SectorController', 'update']);
$this->delete('sectors/:id', ['SectorController', 'delete']);
} }
public function handle(): void { public function handle(): void {

5
api/src/Core/Session.php Normal file → Executable file
View File

@@ -27,6 +27,7 @@ class Session {
public static function login(array $userData): void { public static function login(array $userData): void {
$_SESSION['user_id'] = $userData['id']; $_SESSION['user_id'] = $userData['id'];
$_SESSION['user_email'] = $userData['email'] ?? ''; $_SESSION['user_email'] = $userData['email'] ?? '';
$_SESSION['entity_id'] = $userData['fk_entite'] ?? null;
$_SESSION['authenticated'] = true; $_SESSION['authenticated'] = true;
$_SESSION['last_activity'] = time(); $_SESSION['last_activity'] = time();
@@ -51,6 +52,10 @@ class Session {
return $_SESSION['user_email'] ?? null; return $_SESSION['user_email'] ?? null;
} }
public static function getEntityId(): ?int {
return $_SESSION['entity_id'] ?? null;
}
public static function requireAuth(): void { public static function requireAuth(): void {
if (!self::isAuthenticated()) { if (!self::isAuthenticated()) {
// Log détaillé pour le debug // Log détaillé pour le debug

View File

@@ -0,0 +1,345 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/LogService.php';
class AddressService {
private ?PDO $addressesDb = null;
private PDO $mainDb;
private LogService $logService;
public function __construct() {
$this->logService = new LogService();
try {
$this->addressesDb = AddressesDatabase::getInstance();
$this->logService->info('[AddressService] Connexion à la base d\'adresses réussie');
} catch (\Exception $e) {
// Si la connexion échoue, on continue sans la base d'adresses
$this->logService->error('[AddressService] Connexion à la base d\'adresses impossible', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
$this->addressesDb = null;
}
$this->mainDb = Database::getInstance();
}
/**
* Vérifie si la connexion à la base d'adresses est active
* @return bool
*/
public function isConnected(): bool {
return $this->addressesDb !== null;
}
/**
* Détermine le département de l'entité courante
*
* @param int|null $entityId ID de l'entité
* @return string|null Code département (ex: "22", "23")
*/
private function getDepartmentForEntity(?int $entityId = null): ?string {
if (!$entityId) {
$entityId = $_SESSION['entity_id'] ?? null;
}
if (!$entityId) {
return null;
}
try {
$query = "SELECT departement FROM entites WHERE id = :entity_id";
$stmt = $this->mainDb->prepare($query);
$stmt->execute(['entity_id' => $entityId]);
$result = $stmt->fetch();
return $result ? $result['departement'] : null;
} catch (\Exception $e) {
return null;
}
}
/**
* Récupère toutes les adresses contenues dans un polygone défini par des coordonnées
* Gère automatiquement les secteurs multi-départements
*
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
* @return array Array des adresses trouvées
*/
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array {
// Si pas de connexion à la base d'adresses, retourner un tableau vide
if (!$this->addressesDb) {
$this->logService->error('[AddressService] Pas de connexion à la base d\'adresses externe', [
'entity_id' => $entityId
]);
return [];
}
$this->logService->info('[AddressService] Début recherche adresses', [
'entity_id' => $entityId,
'nb_coordinates' => count($coordinates)
]);
if (count($coordinates) < 3) {
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
}
// D'abord, déterminer tous les départements touchés par ce secteur
require_once __DIR__ . '/DepartmentBoundaryService.php';
$boundaryService = new \DepartmentBoundaryService();
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
if (empty($departmentsTouched)) {
// Si aucun département n'est trouvé par analyse spatiale,
// chercher d'abord dans le département de l'entité et ses limitrophes
$entityDept = $this->getDepartmentForEntity($entityId);
$this->logService->info('[AddressService] Département de l\'entité', [
'departement' => $entityDept
]);
if (!$entityDept) {
$this->logService->error('[AddressService] Impossible de déterminer le département de l\'entité', [
'entity_id' => $entityId
]);
throw new RuntimeException("Impossible de déterminer le département");
}
// Obtenir les départements prioritaires (entité + limitrophes)
$priorityDepts = $boundaryService->getPriorityDepartments($entityDept);
// Log pour debug
$this->logService->warning('[AddressService] Aucun département trouvé par analyse spatiale', [
'departements_prioritaires' => implode(', ', $priorityDepts)
]);
// Utiliser les départements prioritaires pour la recherche
$departmentsTouched = [];
foreach ($priorityDepts as $deptCode) {
$departmentsTouched[] = ['code_dept' => $deptCode];
}
}
// Créer le polygone SQL à partir des coordonnées
$polygonPoints = [];
foreach ($coordinates as $coord) {
if (!isset($coord[0]) || !isset($coord[1])) {
throw new InvalidArgumentException("Chaque coordonnée doit avoir une latitude et une longitude");
}
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
}
// Fermer le polygone
$polygonPoints[] = $polygonPoints[0];
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
// Collecter les adresses de tous les départements touchés
$allAddresses = [];
foreach ($departmentsTouched as $dept) {
$deptCode = $dept['code_dept'];
$tableName = "cp" . $deptCode;
try {
// Requête pour récupérer les adresses dans le polygone pour ce département
$sql = "SELECT
id,
numero,
rue as voie,
cp as code_postal,
ville as commune,
gps_lat as latitude,
gps_lng as longitude,
x,
y,
code_insee,
nom_ld,
ville_acheminement,
rue_afnor,
source,
certification,
:dept_code as departement
FROM `$tableName`
WHERE ST_Contains(
ST_GeomFromText(:polygon, 4326),
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8)))
)
AND gps_lat != ''
AND gps_lng != ''";
$stmt = $this->addressesDb->prepare($sql);
$stmt->execute([
'polygon' => $polygonString,
'dept_code' => $deptCode
]);
$addresses = $stmt->fetchAll();
// Ajouter les adresses à la collection globale
foreach ($addresses as $address) {
$allAddresses[] = $address;
}
// Log pour debug
$this->logService->info('[AddressService] Recherche dans table', [
'table' => $tableName,
'departement' => $deptCode,
'nb_adresses' => count($addresses)
]);
} catch (PDOException $e) {
// Log l'erreur mais continue avec les autres départements
$this->logService->error('[AddressService] Erreur SQL', [
'table' => $tableName,
'departement' => $deptCode,
'error' => $e->getMessage(),
'code' => $e->getCode()
]);
}
}
$this->logService->info('[AddressService] Fin recherche adresses', [
'total_adresses' => count($allAddresses)
]);
return $allAddresses;
}
/**
* Récupère les adresses dans un rayon autour d'un point
*
* @param float $latitude Latitude du centre
* @param float $longitude Longitude du centre
* @param float $radiusMeters Rayon en mètres
* @param int|null $entityId ID de l'entité (pour déterminer le département)
* @return array Array des adresses trouvées
*/
public function getAddressesInRadius(float $latitude, float $longitude, float $radiusMeters, ?int $entityId = null): array {
// Déterminer le département
$dept = $this->getDepartmentForEntity($entityId);
if (!$dept) {
throw new RuntimeException("Impossible de déterminer le département de l'entité");
}
// Nom de la table selon le département
$tableName = "cp" . $dept;
try {
// Utiliser ST_Distance_Sphere pour calculer la distance en mètres
$sql = "SELECT
id,
numero,
rue as voie,
cp as code_postal,
ville as commune,
gps_lat as latitude,
gps_lng as longitude,
ST_Distance_Sphere(
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))),
ST_GeomFromText(:point, 4326)
) as distance
FROM `$tableName`
WHERE ST_Distance_Sphere(
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))),
ST_GeomFromText(:point, 4326)
) <= :radius
AND gps_lat != ''
AND gps_lng != ''
ORDER BY distance";
$point = "POINT($longitude $latitude)";
$stmt = $this->addressesDb->prepare($sql);
$stmt->execute([
'point' => $point,
'radius' => $radiusMeters
]);
return $stmt->fetchAll();
} catch (PDOException $e) {
throw new RuntimeException("Erreur lors de la récupération des adresses dans la table $tableName : " . $e->getMessage());
}
}
/**
* Compte le nombre d'adresses dans un polygone
* Gère automatiquement les secteurs multi-départements
*
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
* @return int Nombre d'adresses
*/
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int {
// Si pas de connexion à la base d'adresses, retourner 0
if (!$this->addressesDb) {
error_log("AddressService: Pas de connexion à la base d'adresses, retour de 0 adresses");
return 0;
}
if (count($coordinates) < 3) {
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
}
// D'abord, déterminer tous les départements touchés par ce secteur
require_once __DIR__ . '/DepartmentBoundaryService.php';
$boundaryService = new \DepartmentBoundaryService();
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
if (empty($departmentsTouched)) {
// Si aucun département n'est trouvé, utiliser le département de l'entité
$dept = $this->getDepartmentForEntity($entityId);
if (!$dept) {
throw new RuntimeException("Impossible de déterminer le département");
}
$departmentsTouched = [['code_dept' => $dept]];
}
// Créer le polygone SQL à partir des coordonnées
$polygonPoints = [];
foreach ($coordinates as $coord) {
if (!isset($coord[0]) || !isset($coord[1])) {
throw new InvalidArgumentException("Chaque coordonnée doit avoir une latitude et une longitude");
}
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
}
// Fermer le polygone
$polygonPoints[] = $polygonPoints[0];
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
// Compter les adresses dans tous les départements touchés
$totalCount = 0;
foreach ($departmentsTouched as $dept) {
$deptCode = $dept['code_dept'];
$tableName = "cp" . $deptCode;
try {
$sql = "SELECT COUNT(*) as count
FROM `$tableName`
WHERE ST_Contains(
ST_GeomFromText(:polygon, 4326),
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8)))
)
AND gps_lat != ''
AND gps_lng != ''";
$stmt = $this->addressesDb->prepare($sql);
$stmt->execute(['polygon' => $polygonString]);
$result = $stmt->fetch();
$deptCount = (int)$result['count'];
$totalCount += $deptCount;
// Log pour debug
error_log("Département $deptCode : $deptCount adresses comptées");
} catch (PDOException $e) {
// Log l'erreur mais continue avec les autres départements
error_log("Erreur de comptage pour le département $deptCode : " . $e->getMessage());
}
}
return $totalCount;
}
}

0
api/src/Services/ApiService.php Normal file → Executable file
View File

0
api/src/Services/BackupEncryptionService.php Normal file → Executable file
View File

View File

@@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
class DepartmentBoundaryService {
private PDO $db;
public function __construct() {
$this->db = Database::getInstance();
}
/**
* Vérifie si un polygone (secteur) est entièrement contenu dans un département
*
* @param array $sectorCoordinates Coordonnées du secteur [[lat, lng], ...]
* @param string $departmentCode Code du département (22, 29, etc.)
* @return array ['is_contained' => bool, 'message' => string, 'intersecting_departments' => array]
*/
public function checkSectorInDepartment(array $sectorCoordinates, string $departmentCode): array {
if (count($sectorCoordinates) < 3) {
return [
'is_contained' => false,
'message' => 'Un secteur doit avoir au moins 3 points',
'intersecting_departments' => []
];
}
// Créer le polygone du secteur
$polygonPoints = [];
foreach ($sectorCoordinates as $coord) {
if (!isset($coord[0]) || !isset($coord[1])) {
return [
'is_contained' => false,
'message' => 'Coordonnées invalides',
'intersecting_departments' => []
];
}
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // longitude latitude
}
// Fermer le polygone
$polygonPoints[] = $polygonPoints[0];
$sectorPolygon = 'POLYGON((' . implode(',', $polygonPoints) . '))';
try {
// 1. Vérifier si le secteur est entièrement dans le département cible
$sql = "SELECT
code_dept,
nom_dept,
ST_Contains(contour, ST_GeomFromText(:sector_polygon, 4326)) as is_contained,
ST_Intersects(contour, ST_GeomFromText(:sector_polygon, 4326)) as intersects
FROM x_departements
WHERE code = :dept_code
AND contour IS NOT NULL";
$stmt = $this->db->prepare($sql);
$stmt->execute([
'sector_polygon' => $sectorPolygon,
'dept_code' => $departmentCode
]);
$targetDept = $stmt->fetch();
if (!$targetDept) {
return [
'is_contained' => false,
'message' => "Le département $departmentCode n'a pas de contour défini",
'intersecting_departments' => []
];
}
// 2. Si le secteur n'est pas entièrement contenu, trouver tous les départements qu'il touche
if (!$targetDept['is_contained']) {
$sql = "SELECT
code_dept,
nom_dept,
ST_Area(ST_Intersection(contour, ST_GeomFromText(:sector_polygon1, 4326))) /
ST_Area(ST_GeomFromText(:sector_polygon2, 4326)) * 100 as percentage_overlap
FROM x_departements_contours
WHERE ST_Intersects(contour, ST_GeomFromText(:sector_polygon3, 4326))
ORDER BY percentage_overlap DESC";
$stmt = $this->db->prepare($sql);
$stmt->execute([
'sector_polygon1' => $sectorPolygon,
'sector_polygon2' => $sectorPolygon,
'sector_polygon3' => $sectorPolygon
]);
$intersectingDepts = $stmt->fetchAll(\PDO::FETCH_ASSOC);
// Formater le message
$deptsList = array_map(function($d) {
return sprintf("%s (%s) : %.1f%%",
$d['nom_dept'],
$d['code_dept'],
$d['percentage_overlap']
);
}, $intersectingDepts);
return [
'is_contained' => false,
'message' => "Le secteur déborde du département {$targetDept['nom_dept']}. Il est à cheval sur : " . implode(', ', $deptsList),
'intersecting_departments' => $intersectingDepts
];
}
return [
'is_contained' => true,
'message' => "Le secteur est entièrement contenu dans le département {$targetDept['nom_dept']}",
'intersecting_departments' => [$targetDept]
];
} catch (\PDOException $e) {
throw new RuntimeException("Erreur lors de la vérification des limites départementales : " . $e->getMessage());
}
}
/**
* Récupère les départements qui intersectent avec un secteur
*
* @param array $sectorCoordinates Coordonnées du secteur [[lat, lng], ...]
* @return array Liste des départements avec leur pourcentage de recouvrement
*/
public function getDepartmentsForSector(array $sectorCoordinates): array {
if (count($sectorCoordinates) < 3) {
return [];
}
// Créer le polygone du secteur
$polygonPoints = [];
foreach ($sectorCoordinates as $coord) {
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // longitude latitude
}
$polygonPoints[] = $polygonPoints[0];
$sectorPolygon = 'POLYGON((' . implode(',', $polygonPoints) . '))';
try {
$sql = "SELECT
code_dept,
nom_dept,
ST_Area(ST_Intersection(contour, ST_GeomFromText(:sector_polygon1, 4326))) /
ST_Area(ST_GeomFromText(:sector_polygon2, 4326)) * 100 as percentage_overlap
FROM x_departements_contours
WHERE ST_Intersects(contour, ST_GeomFromText(:sector_polygon3, 4326))
ORDER BY percentage_overlap DESC";
$stmt = $this->db->prepare($sql);
$stmt->execute([
'sector_polygon1' => $sectorPolygon,
'sector_polygon2' => $sectorPolygon,
'sector_polygon3' => $sectorPolygon
]);
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
throw new RuntimeException("Erreur lors de la recherche des départements : " . $e->getMessage());
}
}
/**
* Vérifie si les contours des départements sont chargés
*
* @return array ['loaded' => bool, 'count' => int, 'missing' => array]
*/
public function checkDepartmentContoursStatus(): array {
try {
// Compter les départements avec contours
$sql = "SELECT COUNT(*) as count FROM x_departements_contours";
$stmt = $this->db->query($sql);
$count = $stmt->fetch()['count'];
// Récupérer la liste des départements utilisés dans les entités
$sql = "SELECT DISTINCT departement
FROM entites
WHERE departement IS NOT NULL
ORDER BY departement";
$stmt = $this->db->query($sql);
$usedDepts = $stmt->fetchAll(\PDO::FETCH_COLUMN);
// Vérifier lesquels ont des contours
$sql = "SELECT code_dept FROM x_departements_contours WHERE code_dept IN ('" . implode("','", $usedDepts) . "')";
$stmt = $this->db->query($sql);
$loadedDepts = $stmt->fetchAll(\PDO::FETCH_COLUMN);
$missingDepts = array_diff($usedDepts, $loadedDepts);
return [
'loaded' => count($missingDepts) === 0,
'count' => $count,
'total_used' => count($usedDepts),
'missing' => array_values($missingDepts)
];
} catch (\PDOException $e) {
return [
'loaded' => false,
'count' => 0,
'total_used' => 0,
'missing' => []
];
}
}
/**
* Récupère les départements limitrophes d'un département donné
*
* @param string $departmentCode Code du département
* @return array Array des codes départements limitrophes
*/
public function getAdjacentDepartments(string $departmentCode): array {
try {
$sql = "SELECT dept_limitrophes FROM x_departements WHERE code = :dept_code AND chk_active = 1";
$stmt = $this->db->prepare($sql);
$stmt->execute(['dept_code' => $departmentCode]);
$result = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$result || empty($result['dept_limitrophes'])) {
return [];
}
// Convertir la chaîne CSV en array
return array_map('trim', explode(',', $result['dept_limitrophes']));
} catch (PDOException $e) {
error_log("Erreur lors de la récupération des départements limitrophes : " . $e->getMessage());
return [];
}
}
/**
* Récupère les départements à prioriser pour la recherche
* (département de l'entité + ses limitrophes)
*
* @param string $entityDepartment Code du département de l'entité
* @return array Array des codes départements à prioriser
*/
public function getPriorityDepartments(string $entityDepartment): array {
$priorityDepts = [$entityDepartment]; // Commencer par le département de l'entité
// Ajouter les départements limitrophes
$adjacentDepts = $this->getAdjacentDepartments($entityDepartment);
$priorityDepts = array_merge($priorityDepts, $adjacentDepts);
// Retourner sans doublons
return array_unique($priorityDepts);
}
}

0
api/src/Services/EmailTemplates.php Normal file → Executable file
View File

0
api/src/Services/ExportService.php Normal file → Executable file
View File

0
api/src/Services/FileService.php Normal file → Executable file
View File

48
api/src/Services/LogService.php Normal file → Executable file
View File

@@ -26,6 +26,19 @@ class LogService {
$defaultMetadata['app_identifier'] = $clientInfo['appIdentifier']; $defaultMetadata['app_identifier'] = $clientInfo['appIdentifier'];
} }
// Ajouter les informations de session si disponibles
if (session_status() === PHP_SESSION_ACTIVE) {
if (isset($_SESSION['user_id'])) {
$defaultMetadata['user_id'] = $_SESSION['user_id'];
}
if (isset($_SESSION['entity_id'])) {
$defaultMetadata['entity_id'] = $_SESSION['entity_id'];
}
if (isset($_SESSION['operation_id'])) {
$defaultMetadata['operation_id'] = $_SESSION['operation_id'];
}
}
$metadata = array_merge_recursive($defaultMetadata, $metadata); $metadata = array_merge_recursive($defaultMetadata, $metadata);
$logData = [ $logData = [
@@ -73,15 +86,31 @@ class LogService {
// timestamp;browser.name@browser.version;os.name@os.version;client_type;$metadata;$message // timestamp;browser.name@browser.version;os.name@os.version;client_type;$metadata;$message
$timestamp = date('Y-m-d\TH:i:s'); $timestamp = date('Y-m-d\TH:i:s');
$browserInfo = $clientInfo['browser']['name'] . '@' . $clientInfo['browser']['version']; $browserInfo = $clientInfo['browser']['name'] . '@' . $clientInfo['browser']['version'];
$osInfo = $clientInfo['os']['name'] . '@' . $clientInfo['os']['version'];
// Ne pas afficher l'OS s'il est unknown
$osInfo = '';
if ($clientInfo['os']['name'] !== 'unknown' && $clientInfo['os']['version'] !== 'unknown') {
$osInfo = $clientInfo['os']['name'] . '@' . $clientInfo['os']['version'];
}
// Extraire le niveau de log // Extraire le niveau de log
$level = isset($metadata['level']) ? (is_array($metadata['level']) ? 'info' : $metadata['level']) : 'info'; $level = isset($metadata['level']) ? (is_array($metadata['level']) ? 'info' : $metadata['level']) : 'info';
// Préparer les métadonnées supplémentaires (exclure celles déjà incluses dans le format) // Préparer les métadonnées supplémentaires (exclure celles déjà incluses dans le format)
$additionalMetadata = []; $additionalMetadata = [];
// Ajouter user_id, entity_id et operation_id en premier s'ils existent
$priorityKeys = ['user_id', 'entity_id', 'operation_id'];
foreach ($priorityKeys as $key) {
if (isset($metadata[$key]) && !is_array($metadata[$key])) {
$additionalMetadata[$key] = $metadata[$key];
}
}
// Ajouter les autres métadonnées
foreach ($metadata as $key => $value) { foreach ($metadata as $key => $value) {
if (!in_array($key, ['browser', 'os', 'client_type', 'side', 'version', 'level', 'environment', 'client'])) { if (!in_array($key, ['browser', 'os', 'client_type', 'side', 'version', 'level', 'environment', 'client'])
&& !in_array($key, $priorityKeys)) {
if (is_array($value)) { if (is_array($value)) {
$additionalMetadata[$key] = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $additionalMetadata[$key] = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} else { } else {
@@ -114,4 +143,19 @@ class LogService {
error_log("Erreur lors de l'écriture des logs: " . $e->getMessage()); error_log("Erreur lors de l'écriture des logs: " . $e->getMessage());
} }
} }
public function info(string $message, array $metadata = []): void {
$metadata['level'] = 'info';
self::log($message, $metadata);
}
public function warning(string $message, array $metadata = []): void {
$metadata['level'] = 'warning';
self::log($message, $metadata);
}
public function error(string $message, array $metadata = []): void {
$metadata['level'] = 'error';
self::log($message, $metadata);
}
} }

0
api/src/Services/OperationDataService.php Normal file → Executable file
View File

0
api/src/Utils/ClientDetector.php Normal file → Executable file
View File

View File

@@ -0,0 +1,54 @@
<?php
/**
* Script de test de la connexion à la base de données des adresses
*/
require_once __DIR__ . '/src/Config/AppConfig.php';
require_once __DIR__ . '/src/Core/AddressesDatabase.php';
echo "Test de connexion à la base de données des adresses\n";
echo "==================================================\n\n";
try {
// Initialiser la configuration
$appConfig = AppConfig::getInstance();
$addressesConfig = $appConfig->getAddressesDatabaseConfig();
echo "Configuration:\n";
echo "- Environnement: " . $appConfig->getEnvironment() . "\n";
echo "- Host: " . $addressesConfig['host'] . "\n";
echo "- Database: " . $addressesConfig['name'] . "\n";
echo "- Username: " . $addressesConfig['username'] . "\n\n";
// Initialiser la connexion
AddressesDatabase::init($addressesConfig);
$db = AddressesDatabase::getInstance();
echo "✓ Connexion réussie!\n\n";
// Tester une requête simple
echo "Test de requête...\n";
$stmt = $db->query("SELECT COUNT(*) as total FROM adresses LIMIT 1");
$result = $stmt->fetch();
echo "✓ Nombre total d'adresses: " . number_format($result['total']) . "\n\n";
// Tester les fonctions géospatiales
echo "Test des fonctions géospatiales...\n";
$stmt = $db->query("SELECT ST_AsText(ST_GeomFromText('POINT(2.3522 48.8566)', 4326)) as point");
$result = $stmt->fetch();
echo "✓ Fonctions géospatiales disponibles: " . $result['point'] . "\n\n";
// Afficher les colonnes de la table adresses
echo "Structure de la table adresses:\n";
$stmt = $db->query("DESCRIBE adresses");
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($columns as $column) {
echo "- " . $column['Field'] . " (" . $column['Type'] . ")\n";
}
} catch (Exception $e) {
echo "✗ Erreur: " . $e->getMessage() . "\n";
echo "Trace:\n" . $e->getTraceAsString() . "\n";
}
echo "\n";

110
api/test_addresses_dept.php Normal file
View File

@@ -0,0 +1,110 @@
<?php
/**
* Script de test de la connexion à la base de données des adresses
* Adapté pour la structure par département
*/
require_once __DIR__ . '/src/Config/AppConfig.php';
require_once __DIR__ . '/src/Core/Database.php';
require_once __DIR__ . '/src/Core/AddressesDatabase.php';
echo "Test de connexion à la base de données des adresses par département\n";
echo "==================================================================\n\n";
try {
// Initialiser la configuration
$appConfig = AppConfig::getInstance();
$addressesConfig = $appConfig->getAddressesDatabaseConfig();
echo "Configuration:\n";
echo "- Environnement: " . $appConfig->getEnvironment() . "\n";
echo "- Host: " . $addressesConfig['host'] . "\n";
echo "- Database: " . $addressesConfig['name'] . "\n";
echo "- Username: " . $addressesConfig['username'] . "\n\n";
// Initialiser les connexions
Database::init($appConfig->getDatabaseConfig());
AddressesDatabase::init($addressesConfig);
$mainDb = Database::getInstance();
$addressesDb = AddressesDatabase::getInstance();
echo "✓ Connexion réussie aux deux bases!\n\n";
// Lister les tables cp* disponibles
echo "Tables d'adresses disponibles:\n";
$stmt = $addressesDb->query("SHOW TABLES LIKE 'cp%'");
$tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
foreach ($tables as $table) {
// Extraire le numéro de département
$dept = str_replace('cp', '', $table);
// Compter les lignes
$countStmt = $addressesDb->query("SELECT COUNT(*) as total FROM `$table`");
$count = $countStmt->fetch()['total'];
echo "- $table (département $dept): " . number_format($count) . " adresses\n";
}
echo "\n";
// Tester avec une entité
echo "Test de récupération du département d'une entité:\n";
$testEntityId = 1; // Changez selon votre base
$stmt = $mainDb->prepare("SELECT id, nom, departement FROM entites WHERE id = :id");
$stmt->execute(['id' => $testEntityId]);
$entity = $stmt->fetch();
if ($entity) {
echo "- Entité #{$entity['id']}: {$entity['nom']} (département {$entity['departement']})\n";
$tableName = "cp" . $entity['departement'];
// Vérifier si la table existe
if (in_array($tableName, $tables)) {
echo "✓ Table $tableName existe\n";
// Tester une requête sur cette table
$stmt = $addressesDb->query("SELECT numero, rue, cp, ville, gps_lat, gps_lng
FROM `$tableName`
WHERE gps_lat != '' AND gps_lng != ''
LIMIT 3");
$addresses = $stmt->fetchAll();
echo "\nExemples d'adresses dans le département {$entity['departement']}:\n";
foreach ($addresses as $addr) {
echo "- {$addr['numero']} {$addr['rue']}, {$addr['cp']} {$addr['ville']} ";
echo "(lat: {$addr['gps_lat']}, lng: {$addr['gps_lng']})\n";
}
// Tester une requête géospatiale
echo "\nTest de requête géospatiale:\n";
$testLat = $addresses[0]['gps_lat'] ?? 48.8566;
$testLng = $addresses[0]['gps_lng'] ?? 2.3522;
$sql = "SELECT COUNT(*) as total
FROM `$tableName`
WHERE ST_Distance_Sphere(
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))),
POINT(:lng, :lat)
) <= 1000
AND gps_lat != '' AND gps_lng != ''";
$stmt = $addressesDb->prepare($sql);
$stmt->execute(['lat' => $testLat, 'lng' => $testLng]);
$result = $stmt->fetch();
echo "✓ Adresses dans un rayon de 1km autour de ($testLat, $testLng): " . $result['total'] . "\n";
} else {
echo "✗ Table $tableName n'existe pas dans la base adresses\n";
}
}
} catch (Exception $e) {
echo "✗ Erreur: " . $e->getMessage() . "\n";
echo "Trace:\n" . $e->getTraceAsString() . "\n";
}
echo "\n";

59
api/test_api_geo.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
/**
* Test rapide de l'API geo.api.gouv.fr
*/
echo "Test des différentes URLs de l'API geo.api.gouv.fr\n";
echo "=================================================\n\n";
$urls = [
"fields=contour" => "https://geo.api.gouv.fr/departements/22?fields=contour",
"geometry=contour" => "https://geo.api.gouv.fr/departements/22?geometry=contour",
"fields=contour,nom" => "https://geo.api.gouv.fr/departements/22?fields=contour,nom",
"fields=nom&geometry=contour" => "https://geo.api.gouv.fr/departements/22?fields=nom&geometry=contour",
];
$context = stream_context_create([
'http' => [
'timeout' => 10,
'header' => "User-Agent: Geosector/1.0\r\n"
]
]);
foreach ($urls as $type => $url) {
echo "Test : $type\n";
echo "URL : $url\n";
$response = @file_get_contents($url, false, $context);
if ($response === false) {
echo "✗ Erreur\n";
} else {
$data = json_decode($response, true);
echo "✓ Réponse reçue\n";
// Afficher les clés du premier niveau
echo "Clés : " . implode(', ', array_keys($data)) . "\n";
// Vérifier la présence du contour
if (isset($data['contour'])) {
echo "→ Contour présent (type: " . ($data['contour']['type'] ?? 'inconnu') . ")\n";
if (isset($data['contour']['coordinates'])) {
$coords = $data['contour']['coordinates'];
$pointCount = is_array($coords[0]) ? count($coords[0]) : count($coords);
echo "→ Nombre de points : $pointCount\n";
}
} elseif (isset($data['geometry'])) {
echo "→ Geometry présent (type: " . ($data['geometry']['type'] ?? 'inconnu') . ")\n";
if (isset($data['geometry']['coordinates'])) {
$coords = $data['geometry']['coordinates'];
$pointCount = is_array($coords[0]) ? count($coords[0]) : count($coords);
echo "→ Nombre de points : $pointCount\n";
}
} else {
echo "→ Pas de contour ou geometry\n";
}
}
echo "\n";
}

Some files were not shown because too many files have changed in this diff Show More