AUTO-FRANCE-PARTS commited on
Commit
ee194aa
·
1 Parent(s): b411033

feat: Implement nearby parcel retrieval feature with sampling and add parcel repository

Browse files
.gitignore CHANGED
@@ -1 +1,2 @@
1
- */__pycache__/*
 
 
1
+ **/__pycache__/**
2
+
app.py CHANGED
@@ -1,6 +1,8 @@
1
  from fastapi import FastAPI, HTTPException, Query
2
- from typing import Dict, Any
3
  from api_sources.lexicon import LexiconAPI
 
 
4
 
5
  app = FastAPI(
6
  title="Data API",
@@ -11,6 +13,9 @@ app = FastAPI(
11
  # Instance de l'API Lexicon
12
  lexicon_api = LexiconAPI()
13
 
 
 
 
14
  @app.get("/")
15
  def greet_json():
16
  return {"Hello": "World!"}
@@ -40,3 +45,42 @@ async def get_parcel_identifier(
40
  status_code=500,
41
  detail=f"Erreur lors de la récupération des données de parcelle: {str(e)}"
42
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from fastapi import FastAPI, HTTPException, Query
2
+ from typing import Dict, Any, List
3
  from api_sources.lexicon import LexiconAPI
4
+ from domain.repositories.parcel_repository import ParcelRepository
5
+ from domain.use_cases.get_nearby_parcels_by_sampling import GetNearbyParcelsBySamplingUseCase
6
 
7
  app = FastAPI(
8
  title="Data API",
 
13
  # Instance de l'API Lexicon
14
  lexicon_api = LexiconAPI()
15
 
16
+ # Repository et use cases
17
+ parcel_repository = ParcelRepository(lexicon_api)
18
+
19
  @app.get("/")
20
  def greet_json():
21
  return {"Hello": "World!"}
 
45
  status_code=500,
46
  detail=f"Erreur lors de la récupération des données de parcelle: {str(e)}"
47
  )
48
+
49
+ @app.get("/tools/get_nearby_parcel")
50
+ async def get_nearby_parcels(
51
+ latitude: float = Query(..., description="Latitude du point central en degrés décimaux"),
52
+ longitude: float = Query(..., description="Longitude du point central en degrés décimaux"),
53
+ radius_m: float = Query(200, description="Rayon de recherche en mètres", gt=0),
54
+ step_m: float = Query(100, description="Pas d'échantillonnage en mètres (densité des points)", gt=0)
55
+ ) -> List[Dict[str, Any]]:
56
+ """
57
+ Récupère les parcelles à proximité d'un point donné par échantillonnage.
58
+
59
+ Args:
60
+ latitude: Latitude du point central en degrés décimaux
61
+ longitude: Longitude du point central en degrés décimaux
62
+ radius_m: Rayon de recherche en mètres
63
+ step_m: Pas d'échantillonnage en mètres (plus petit = plus dense)
64
+
65
+ Returns:
66
+ Liste des parcelles trouvées dans la zone
67
+ """
68
+ try:
69
+
70
+ # Création du use case avec les paramètres
71
+ get_nearby_parcels_use_case = GetNearbyParcelsBySamplingUseCase(
72
+ parcel_repository=parcel_repository,
73
+ buffer_radius_m=radius_m,
74
+ step_m=step_m
75
+ )
76
+
77
+ # Exécution du use case
78
+ nearby_parcels = await get_nearby_parcels_use_case(latitude, longitude)
79
+
80
+ return nearby_parcels
81
+
82
+ except Exception as e:
83
+ raise HTTPException(
84
+ status_code=500,
85
+ detail=f"Erreur lors de la récupération des parcelles à proximité: {str(e)}"
86
+ )
domain/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
domain/repositories/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
domain/repositories/parcel_repository.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Any, Optional
2
+ from api_sources.lexicon import LexiconAPI
3
+
4
+
5
+ class ParcelRepository:
6
+ """Repository pour récupérer les données de parcelles depuis les API externes."""
7
+
8
+ def __init__(self, lexicon_api: LexiconAPI):
9
+ self.lexicon_api = lexicon_api
10
+
11
+ async def get_parcel_from_point(self, latitude: float, longitude: float) -> Optional[Dict[str, Any]]:
12
+ """
13
+ Récupère les informations de parcelle pour un point donné.
14
+
15
+ Args:
16
+ latitude: Latitude en degrés décimaux
17
+ longitude: Longitude en degrés décimaux
18
+
19
+ Returns:
20
+ Dict contenant les données de la parcelle ou None si aucune parcelle trouvée
21
+ """
22
+ try:
23
+ parcel_data = await self.lexicon_api.get_parcel_from_lat_lon(latitude, longitude)
24
+
25
+ # Si l'API ne retourne pas de données de parcelle, on retourne None
26
+ if not parcel_data or not parcel_data.get('@id'):
27
+ return None
28
+
29
+ return parcel_data
30
+
31
+ except Exception as e:
32
+ # Log de l'erreur mais on continue avec les autres points
33
+ print(f"Erreur lors de la récupération de la parcelle en ({latitude}, {longitude}): {e}")
34
+ return None
domain/use_cases/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
domain/use_cases/get_nearby_parcels_by_sampling.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from shapely.geometry import Point
2
+ from shapely.ops import transform
3
+ from pyproj import Transformer
4
+ from typing import List, Set
5
+ import math
6
+ import asyncio
7
+
8
+
9
+ class GetNearbyParcelsBySamplingUseCase:
10
+ def __init__(self, parcel_repository, buffer_radius_m: float, step_m: float = 100):
11
+ self.repo = parcel_repository
12
+ self.radius = buffer_radius_m
13
+ self.step = step_m # espacement des points (densité)
14
+ self.to_projected = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True).transform
15
+ self.to_geographic = Transformer.from_crs("EPSG:3857", "EPSG:4326", always_xy=True).transform
16
+
17
+ async def __call__(self, lat: float, lon: float) -> List[dict]:
18
+ center = transform(self.to_projected, Point(lon, lat))
19
+
20
+ sampled_points = self._generate_sampling_points(center)
21
+
22
+ seen_ids: Set[str] = set()
23
+ nearby_parcels: List[dict] = []
24
+
25
+ # Traitement asynchrone en lot pour optimiser les performances
26
+ tasks = []
27
+ for pt in sampled_points:
28
+ lon_, lat_ = transform(self.to_geographic, pt).coords[0]
29
+ tasks.append(self.repo.get_parcel_from_point(lat_, lon_))
30
+
31
+ print(f"len(tasks): {len(tasks)}")
32
+
33
+ # Exécution parallèle des requêtes
34
+ parcel_results = await asyncio.gather(*tasks, return_exceptions=True)
35
+
36
+ for parcel in parcel_results:
37
+ if isinstance(parcel, Exception):
38
+ continue # Ignorer les erreurs et continuer
39
+
40
+ if parcel:
41
+ parcel_id = parcel.get("cadastre", {}).get("id", {}).get("value")
42
+ if parcel_id not in seen_ids:
43
+ nearby_parcels.append(parcel)
44
+ seen_ids.add(parcel_id)
45
+
46
+ return nearby_parcels
47
+
48
+ def _generate_sampling_points(self, center: Point) -> List[Point]:
49
+ points = []
50
+ step = self.step
51
+ radius = self.radius
52
+
53
+ min_x, min_y = center.x - radius, center.y - radius
54
+ max_x, max_y = center.x + radius, center.y + radius
55
+
56
+ for x in range(int(min_x), int(max_x), int(step)):
57
+ for y in range(int(min_y), int(max_y), int(step)):
58
+ pt = Point(x, y)
59
+ if pt.distance(center) <= radius:
60
+ points.append(pt)
61
+ return points
requirements.txt CHANGED
@@ -2,3 +2,7 @@ fastapi
2
  uvicorn[standard]
3
  requests
4
  httpx
 
 
 
 
 
2
  uvicorn[standard]
3
  requests
4
  httpx
5
+
6
+ # Géométrie et traitement spatial
7
+ shapely>=2.0.0
8
+ pyproj>=3.6.0