districts-geolocation
ISSUE #5 : DISTRICTS & GÉOLOCALISATION
🎯 OBJECTIF
Implémenter la carte Fiscométéo avec gestion des districts et géolocalisation interactive.
📖 CONTEXTE DU PROJET
La carte Fiscométéo est la feature phare de SmartAudit. Elle permet de visualiser en temps réel l'état fiscal de chaque district de Côte d'Ivoire avec des données interactives et des statistiques dynamiques.
14 Districts de Côte d'Ivoire
- Abidjan (ABJ) - District économique principal
- Yamoussoukro (YAM) - Capitale politique
- Bouaké (BOU) - Centre du pays
- San-Pédro (SPE) - Port maritime
- Korhogo (KOR) - Nord du pays
- Man (MAN) - Ouest montagneux
- Gagnoa (GAG) - Centre-ouest
- Divo (DIV) - Sud-ouest
- Abengourou (ABE) - Est
- Bondoukou (BON) - Nord-est
- Odienné (ODI) - Nord-ouest
- Daloa (DAL) - Centre-ouest
- Séguéla (SEG) - Centre-nord
- Toumodi (TOU) - Centre
Fonctionnalités de la Carte
- Visualisation par district : Couleurs selon niveau de risque
- Statistiques temps réel : Nombre d'entreprises, alertes, audits
- Filtres interactifs : Par secteur, taille, statut
- Zoom et navigation : Interface intuitive
- Données détaillées : Popup avec informations complètes
🛠️ TÂCHES À IMPLÉMENTER
1. Services de Géolocalisation
// Fichier : service/DistrictService.java
@Service
@Transactional
public class DistrictService {
@Autowired
private DistrictRepository districtRepository;
@Autowired
private CompanyRepository companyRepository;
@Autowired
private AlertRepository alertRepository;
@Autowired
private AuditRepository auditRepository;
public List<DistrictDTO> getAllDistricts() {
List<District> districts = districtRepository.findAll();
return districts.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
public DistrictDetailsDTO getDistrictDetails(Long districtId) {
District district = districtRepository.findById(districtId)
.orElseThrow(() -> new DistrictNotFoundException("District non trouvé"));
// Calculer les statistiques
DistrictStatistics stats = calculateDistrictStatistics(district);
return DistrictDetailsDTO.builder()
.district(convertToDTO(district))
.statistics(stats)
.companies(getDistrictCompanies(district))
.alerts(getDistrictAlerts(district))
.audits(getDistrictAudits(district))
.build();
}
public List<DistrictMapDTO> getDistrictsForMap() {
List<District> districts = districtRepository.findAll();
return districts.stream()
.map(district -> {
DistrictStatistics stats = calculateDistrictStatistics(district);
return DistrictMapDTO.builder()
.id(district.getId())
.code(district.getCode())
.name(district.getName())
.coordinates(parseCoordinates(district.getCoordinates()))
.riskLevel(calculateRiskLevel(stats))
.companyCount(stats.getCompanyCount())
.alertCount(stats.getAlertCount())
.auditCount(stats.getAuditCount())
.complianceScore(stats.getAverageComplianceScore())
.build();
})
.collect(Collectors.toList());
}
public DistrictStatistics calculateDistrictStatistics(District district) {
// Compter les entreprises
long companyCount = companyRepository.countByDistrict(district);
// Compter les alertes actives
long alertCount = alertRepository.countByCompanyDistrictAndStatus(district, AlertStatus.NEW);
// Compter les audits en cours
long auditCount = auditRepository.countByCompanyDistrictAndStatus(district, AuditStatus.IN_PROGRESS);
// Calculer score de conformité moyen
BigDecimal avgCompliance = companyRepository.getAverageComplianceScoreByDistrict(district);
// Calculer montant total des redressements
BigDecimal totalRedressement = auditRepository.getTotalRedressementByDistrict(district);
return DistrictStatistics.builder()
.companyCount(companyCount)
.alertCount(alertCount)
.auditCount(auditCount)
.averageComplianceScore(avgCompliance)
.totalRedressement(totalRedressement)
.riskLevel(calculateRiskLevel(companyCount, alertCount, avgCompliance))
.build();
}
private RiskLevel calculateRiskLevel(DistrictStatistics stats) {
return calculateRiskLevel(stats.getCompanyCount(), stats.getAlertCount(), stats.getAverageComplianceScore());
}
private RiskLevel calculateRiskLevel(long companyCount, long alertCount, BigDecimal avgCompliance) {
// Calculer le niveau de risque basé sur plusieurs facteurs
double alertRatio = companyCount > 0 ? (double) alertCount / companyCount : 0;
double complianceScore = avgCompliance != null ? avgCompliance.doubleValue() : 0;
if (alertRatio > 0.3 || complianceScore < 60) {
return RiskLevel.CRITICAL;
} else if (alertRatio > 0.2 || complianceScore < 70) {
return RiskLevel.HIGH;
} else if (alertRatio > 0.1 || complianceScore < 80) {
return RiskLevel.MEDIUM;
} else {
return RiskLevel.LOW;
}
}
private CoordinatesDTO parseCoordinates(String coordinatesJson) {
try {
ObjectMapper mapper = new ObjectMapper();
JsonNode jsonNode = mapper.readTree(coordinatesJson);
return CoordinatesDTO.builder()
.latitude(jsonNode.get("lat").asDouble())
.longitude(jsonNode.get("lng").asDouble())
.bounds(jsonNode.get("bounds"))
.build();
} catch (Exception e) {
log.error("Erreur parsing coordonnées: {}", e.getMessage());
return null;
}
}
}
2. Controllers REST
// Fichier : controller/DistrictController.java
@RestController
@RequestMapping("/api/districts")
@CrossOrigin(origins = "*")
public class DistrictController {
@Autowired
private DistrictService districtService;
@GetMapping
public ResponseEntity<List<DistrictDTO>> getAllDistricts() {
List<DistrictDTO> districts = districtService.getAllDistricts();
return ResponseEntity.ok(districts);
}
@GetMapping("/{id}")
public ResponseEntity<DistrictDetailsDTO> getDistrictDetails(@PathVariable Long id) {
DistrictDetailsDTO details = districtService.getDistrictDetails(id);
return ResponseEntity.ok(details);
}
@GetMapping("/map")
public ResponseEntity<List<DistrictMapDTO>> getDistrictsForMap() {
List<DistrictMapDTO> districts = districtService.getDistrictsForMap();
return ResponseEntity.ok(districts);
}
@GetMapping("/{id}/companies")
public ResponseEntity<List<CompanyDTO>> getDistrictCompanies(
@PathVariable Long id,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(required = false) Sector sector,
@RequestParam(required = false) RiskLevel riskLevel) {
Pageable pageable = PageRequest.of(page, size);
Page<Company> companies = districtService.getDistrictCompanies(id, sector, riskLevel, pageable);
List<CompanyDTO> companyDTOs = companies.getContent().stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
return ResponseEntity.ok(companyDTOs);
}
@GetMapping("/{id}/statistics")
public ResponseEntity<DistrictStatisticsDTO> getDistrictStatistics(@PathVariable Long id) {
DistrictStatisticsDTO stats = districtService.getDistrictStatistics(id);
return ResponseEntity.ok(stats);
}
@GetMapping("/{id}/alerts")
public ResponseEntity<List<AlertDTO>> getDistrictAlerts(
@PathVariable Long id,
@RequestParam(required = false) AlertStatus status,
@RequestParam(required = false) AlertPriority priority) {
List<AlertDTO> alerts = districtService.getDistrictAlerts(id, status, priority);
return ResponseEntity.ok(alerts);
}
}
3. Configuration Leaflet.js
// Fichier : config/MapConfig.java
@Configuration
public class MapConfig {
@Bean
public MapConfiguration mapConfiguration() {
return MapConfiguration.builder()
.defaultZoom(7)
.defaultCenter(new LatLng(7.5400, -5.5471)) // Centre Côte d'Ivoire
.minZoom(6)
.maxZoom(12)
.tileLayerUrl("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png")
.attribution("© OpenStreetMap contributors")
.build();
}
}
4. DTOs
// Fichier : dto/DistrictDTO.java
public class DistrictDTO {
private Long id;
private String code;
private String name;
private String description;
private CoordinatesDTO coordinates;
private Integer companyCount;
private Integer agentCount;
}
// Fichier : dto/DistrictMapDTO.java
public class DistrictMapDTO {
private Long id;
private String code;
private String name;
private CoordinatesDTO coordinates;
private RiskLevel riskLevel;
private Long companyCount;
private Long alertCount;
private Long auditCount;
private BigDecimal complianceScore;
}
// Fichier : dto/CoordinatesDTO.java
public class CoordinatesDTO {
private Double latitude;
private Double longitude;
private JsonNode bounds; // Limites du district
}
// Fichier : dto/DistrictStatisticsDTO.java
public class DistrictStatisticsDTO {
private Long companyCount;
private Long alertCount;
private Long auditCount;
private BigDecimal averageComplianceScore;
private BigDecimal totalRedressement;
private RiskLevel riskLevel;
private Map<Sector, Long> companiesBySector;
private Map<RiskLevel, Long> companiesByRisk;
private List<MonthlyTrend> monthlyTrends;
}
🔧 OUTILS & TECHNOLOGIES
Frontend (Angular)
- Leaflet.js : Carte interactive
- Angular Material : Interface utilisateur
- Chart.js : Graphiques statistiques
- RxJS : Gestion des observables
Backend (Spring Boot)
- Spring Data JPA : Requêtes géographiques
- Jackson : Sérialisation JSON
- OpenStreetMap : Tiles cartographiques
Dépendances Frontend
{
"dependencies": {
"leaflet": "^1.9.4",
"@types/leaflet": "^1.9.8",
"chart.js": "^4.4.0",
"@angular/material": "^18.0.0"
}
}
🎯 CRITÈRES D'ACCEPTATION
Carte Interactive
-
14 districts : Tous les districts CI affichés -
Couleurs dynamiques : Selon niveau de risque -
Zoom/navigation : Interface fluide -
Popup informations : Détails au clic -
Responsive : Adaptation mobile/desktop
Statistiques Temps Réel
-
Compteurs dynamiques : Entreprises, alertes, audits -
Score conformité : Moyenne par district -
Niveau risque : Calcul automatique -
Tendances : Évolution mensuelle -
Filtres : Par secteur, taille, statut
API REST
-
Endpoints géo : Données pour carte -
Filtres avancés : Par critères multiples -
Pagination : Pour gros volumes -
Performance : < 200ms pour données carte
📁 STRUCTURE DES FICHIERS
src/main/java/ci/dgi/smartaudit/
├── service/
│ ├── DistrictService.java
│ └── GeoDataService.java
├── controller/
│ └── DistrictController.java
├── repository/
│ └── DistrictRepository.java
├── dto/
│ ├── DistrictDTO.java
│ ├── DistrictMapDTO.java
│ ├── CoordinatesDTO.java
│ └── DistrictStatisticsDTO.java
└── config/
└── MapConfig.java
src/main/resources/
├── static/
│ ├── js/
│ │ ├── map.js
│ │ └── statistics.js
│ └── css/
│ └── map.css
└── datasets/
└── districts_coordinates.json
🧪 TESTS À IMPLÉMENTER
Tests Unitaires
@ExtendWith(MockitoExtension.class)
class DistrictServiceTest {
@Test
void calculateRiskLevel_WithHighAlertRatio_ShouldReturnCritical() {
// Test calcul niveau risque
}
@Test
void getDistrictsForMap_ShouldReturnAllDistricts() {
// Test données carte
}
@Test
void parseCoordinates_WithValidJSON_ShouldReturnCoordinates() {
// Test parsing coordonnées
}
}
Tests d'Intégration
@SpringBootTest
@AutoConfigureTestDatabase
class DistrictControllerIntegrationTest {
@Test
void getDistrictsForMap_IntegrationTest() {
// Test endpoint carte
}
}
🚀 DÉMARRAGE RAPIDE
1. Configuration Carte
# application.yml
smartaudit:
map:
default-zoom: 7
default-center:
lat: 7.5400
lng: -5.5471
tile-layer: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
2. Données Districts
// datasets/districts_coordinates.json
[
{
"code": "ABJ",
"name": "Abidjan",
"coordinates": {
"lat": 5.316667,
"lng": -4.033333,
"bounds": [
[5.1, -4.2],
[5.5, -3.8]
]
}
}
]
3. Frontend Angular
// map.component.ts
export class MapComponent implements OnInit {
map: L.Map;
ngOnInit() {
this.initMap();
this.loadDistrictData();
}
private initMap() {
this.map = L.map('map').setView([7.5400, -5.5471], 7);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(this.map);
}
private loadDistrictData() {
this.districtService.getDistrictsForMap().subscribe(districts => {
districts.forEach(district => {
this.addDistrictMarker(district);
});
});
}
}
📊 MÉTRIQUES DE SUCCÈS
- Temps chargement carte : < 3 secondes
- Fluidité navigation : 60 FPS
- Précision géolocalisation : < 100m
- Temps réponse API : < 200ms
🔗 INTÉGRATIONS
Avec Issue #2
- Données entreprises : Géolocalisation par district
- Statistiques : Calculs temps réel
Avec Issue #3
- Alertes : Visualisation par district
- Détection : Zones à risque
Avec Issue #4
- Audits : Répartition géographique
- Agents : Assignation par district
Avec Issue #6
- Notifications : Alertes géographiques
- Rapports : Statistiques par district
⚠️ POINTS D'ATTENTION
- Performance : Optimiser requêtes géographiques
- Données sensibles : Anonymiser si nécessaire
- Connexion : Fonctionnement hors ligne
- Mobile : Interface tactile optimisée
ESTIMATION : 1.5 jours
DIFFICULTÉ : Moyenne
PRIORITÉ : Haute (feature phare)