eco-back 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eco_back/__init__.py +11 -0
- eco_back/api/__init__.py +9 -0
- eco_back/api/client.py +278 -0
- eco_back/api/config.py +58 -0
- eco_back/api/registro.py +91 -0
- eco_back/database/__init__.py +9 -0
- eco_back/database/config.py +58 -0
- eco_back/database/connection.py +198 -0
- eco_back/database/models.py +49 -0
- eco_back/database/postgis.py +350 -0
- eco_back/database/repository.py +152 -0
- eco_back-0.1.0.dist-info/METADATA +191 -0
- eco_back-0.1.0.dist-info/RECORD +16 -0
- eco_back-0.1.0.dist-info/WHEEL +5 -0
- eco_back-0.1.0.dist-info/licenses/LICENSE +21 -0
- eco_back-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gestión de conexiones a base de datos
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional, Any, Dict, List, Union
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import psycopg2
|
|
10
|
+
from psycopg2 import sql
|
|
11
|
+
from psycopg2.extras import RealDictCursor
|
|
12
|
+
PSYCOPG2_AVAILABLE = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
PSYCOPG2_AVAILABLE = False
|
|
15
|
+
|
|
16
|
+
from .config import DatabaseConfig
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DatabaseConnection:
|
|
22
|
+
"""
|
|
23
|
+
Clase para gestionar conexiones a base de datos
|
|
24
|
+
|
|
25
|
+
Ejemplo:
|
|
26
|
+
>>> config = DatabaseConfig(
|
|
27
|
+
... host="localhost",
|
|
28
|
+
... port=3306,
|
|
29
|
+
... database="mi_db",
|
|
30
|
+
... user="usuario",
|
|
31
|
+
... password="password"
|
|
32
|
+
... )
|
|
33
|
+
>>> db = DatabaseConnection(config)
|
|
34
|
+
>>> db.connect()
|
|
35
|
+
>>> resultado = db.execute_query("SELECT * FROM tabla")
|
|
36
|
+
>>> db.close()
|
|
37
|
+
|
|
38
|
+
O usando context manager:
|
|
39
|
+
>>> with DatabaseConnection(config) as db:
|
|
40
|
+
... resultado = db.execute_query("SELECT * FROM tabla")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, config: DatabaseConfig):
|
|
44
|
+
"""
|
|
45
|
+
Inicializa la conexión a la base de datos
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
config: Configuración de la base de datos
|
|
49
|
+
"""
|
|
50
|
+
self.config = config
|
|
51
|
+
self.connection: Optional[Any] = None
|
|
52
|
+
self.cursor: Optional[Any] = None
|
|
53
|
+
|
|
54
|
+
def connect(self) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Establece la conexión a PostgreSQL
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ImportError: Si psycopg2 no está instalado
|
|
60
|
+
Exception: Si no se puede establecer la conexión
|
|
61
|
+
"""
|
|
62
|
+
if not PSYCOPG2_AVAILABLE:
|
|
63
|
+
raise ImportError(
|
|
64
|
+
"psycopg2 no está instalado. Instálalo con: pip install psycopg2-binary"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
self.connection = psycopg2.connect(
|
|
69
|
+
host=self.config.host,
|
|
70
|
+
port=self.config.port,
|
|
71
|
+
dbname=self.config.database,
|
|
72
|
+
user=self.config.user,
|
|
73
|
+
password=self.config.password,
|
|
74
|
+
connect_timeout=self.config.connect_timeout,
|
|
75
|
+
options=f"-c client_encoding={self.config.charset}"
|
|
76
|
+
)
|
|
77
|
+
self.connection.autocommit = False
|
|
78
|
+
|
|
79
|
+
logger.info(f"Conectado a PostgreSQL: {self.config.database}")
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Error al conectar a PostgreSQL: {e}")
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
def close(self) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Cierra la conexión a la base de datos
|
|
87
|
+
"""
|
|
88
|
+
if self.cursor:
|
|
89
|
+
self.cursor.close()
|
|
90
|
+
self.cursor = None
|
|
91
|
+
|
|
92
|
+
if self.connection:
|
|
93
|
+
self.connection.close()
|
|
94
|
+
self.connection = None
|
|
95
|
+
logger.info("Conexión cerrada")
|
|
96
|
+
|
|
97
|
+
def execute_query(self, query: str, params: Optional[Union[tuple, dict]] = None) -> List[Dict[str, Any]]:
|
|
98
|
+
"""
|
|
99
|
+
Ejecuta una consulta SELECT y retorna los resultados
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
query: Consulta SQL a ejecutar
|
|
103
|
+
params: Parámetros para la consulta (opcional)
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
Lista de diccionarios con los resultados
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
Exception: Si hay un error en la consulta
|
|
110
|
+
"""
|
|
111
|
+
if not self.connection:
|
|
112
|
+
raise Exception("No hay conexión a la base de datos")
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
cursor = self.connection.cursor(cursor_factory=RealDictCursor)
|
|
116
|
+
|
|
117
|
+
if params:
|
|
118
|
+
cursor.execute(query, params)
|
|
119
|
+
else:
|
|
120
|
+
cursor.execute(query)
|
|
121
|
+
|
|
122
|
+
results = cursor.fetchall()
|
|
123
|
+
# Convertir RealDictRow a dict normal
|
|
124
|
+
results = [dict(row) for row in results]
|
|
125
|
+
cursor.close()
|
|
126
|
+
|
|
127
|
+
logger.debug(f"Consulta ejecutada: {query}")
|
|
128
|
+
return results
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Error al ejecutar consulta: {e}")
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
def execute_update(self, query: str, params: Optional[Union[tuple, dict]] = None) -> int:
|
|
134
|
+
"""
|
|
135
|
+
Ejecuta una consulta INSERT, UPDATE o DELETE
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
query: Consulta SQL a ejecutar
|
|
139
|
+
params: Parámetros para la consulta (opcional)
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Número de filas afectadas
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
Exception: Si hay un error en la consulta
|
|
146
|
+
"""
|
|
147
|
+
if not self.connection:
|
|
148
|
+
raise Exception("No hay conexión a la base de datos")
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
cursor = self.connection.cursor()
|
|
152
|
+
|
|
153
|
+
if params:
|
|
154
|
+
cursor.execute(query, params)
|
|
155
|
+
else:
|
|
156
|
+
cursor.execute(query)
|
|
157
|
+
|
|
158
|
+
self.connection.commit()
|
|
159
|
+
affected_rows = cursor.rowcount
|
|
160
|
+
cursor.close()
|
|
161
|
+
|
|
162
|
+
logger.debug(f"Actualización ejecutada: {query}, filas afectadas: {affected_rows}")
|
|
163
|
+
return affected_rows
|
|
164
|
+
except Exception as e:
|
|
165
|
+
self.connection.rollback()
|
|
166
|
+
logger.error(f"Error al ejecutar actualización: {e}")
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
@contextmanager
|
|
170
|
+
def transaction(self):
|
|
171
|
+
"""
|
|
172
|
+
Context manager para transacciones
|
|
173
|
+
|
|
174
|
+
Ejemplo:
|
|
175
|
+
>>> with db.transaction():
|
|
176
|
+
... db.execute_update("INSERT INTO tabla VALUES (...)")
|
|
177
|
+
... db.execute_update("UPDATE tabla SET ...")
|
|
178
|
+
"""
|
|
179
|
+
try:
|
|
180
|
+
yield self
|
|
181
|
+
if self.connection:
|
|
182
|
+
self.connection.commit()
|
|
183
|
+
logger.debug("Transacción confirmada")
|
|
184
|
+
except Exception as e:
|
|
185
|
+
if self.connection:
|
|
186
|
+
self.connection.rollback()
|
|
187
|
+
logger.error(f"Transacción revertida: {e}")
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
def __enter__(self):
|
|
191
|
+
"""Context manager entry"""
|
|
192
|
+
self.connect()
|
|
193
|
+
return self
|
|
194
|
+
|
|
195
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
196
|
+
"""Context manager exit"""
|
|
197
|
+
self.close()
|
|
198
|
+
return False
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modelos base para la base de datos
|
|
3
|
+
"""
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Dict, Any, Optional, List
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseModel(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Clase base para modelos de base de datos
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
Convierte el modelo a diccionario
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Diccionario con los datos del modelo
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def from_dict(cls, data: Dict[str, Any]) -> "BaseModel":
|
|
26
|
+
"""
|
|
27
|
+
Crea una instancia del modelo desde un diccionario
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
data: Diccionario con los datos
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Instancia del modelo
|
|
34
|
+
"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def from_db_row(cls, row: Dict[str, Any]) -> "BaseModel":
|
|
40
|
+
"""
|
|
41
|
+
Crea una instancia del modelo desde una fila de la base de datos
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
row: Diccionario con los datos de la fila
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Instancia del modelo
|
|
48
|
+
"""
|
|
49
|
+
pass
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilidades para trabajar con PostGIS en PostgreSQL
|
|
3
|
+
"""
|
|
4
|
+
from typing import Optional, Dict, Any, List, Tuple, Union
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from .connection import DatabaseConnection
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from shapely.geometry import Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon
|
|
13
|
+
from shapely import wkt, wkb
|
|
14
|
+
SHAPELY_AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
SHAPELY_AVAILABLE = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PostGISHelper:
|
|
20
|
+
"""
|
|
21
|
+
Clase helper para operaciones con PostGIS
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, db_connection: DatabaseConnection):
|
|
25
|
+
"""
|
|
26
|
+
Inicializa el helper de PostGIS
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
db_connection: Conexión a la base de datos PostgreSQL
|
|
30
|
+
"""
|
|
31
|
+
self.db = db_connection
|
|
32
|
+
|
|
33
|
+
def enable_postgis(self) -> bool:
|
|
34
|
+
"""
|
|
35
|
+
Habilita la extensión PostGIS en la base de datos
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
True si se habilitó correctamente
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
Exception: Si hay un error al habilitar PostGIS
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
self.db.execute_update("CREATE EXTENSION IF NOT EXISTS postgis")
|
|
45
|
+
logger.info("Extensión PostGIS habilitada")
|
|
46
|
+
return True
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.error(f"Error al habilitar PostGIS: {e}")
|
|
49
|
+
raise
|
|
50
|
+
|
|
51
|
+
def get_postgis_version(self) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Obtiene la versión de PostGIS instalada
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Versión de PostGIS
|
|
57
|
+
"""
|
|
58
|
+
result = self.db.execute_query("SELECT PostGIS_Version()")
|
|
59
|
+
return result[0]['postgis_version'] if result else "No disponible"
|
|
60
|
+
|
|
61
|
+
def create_spatial_index(self, table_name: str, column_name: str = "geom") -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Crea un índice espacial en una columna de geometría
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
table_name: Nombre de la tabla
|
|
67
|
+
column_name: Nombre de la columna de geometría
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
True si se creó correctamente
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
index_name = f"idx_{table_name}_{column_name}"
|
|
74
|
+
query = f"""
|
|
75
|
+
CREATE INDEX IF NOT EXISTS {index_name}
|
|
76
|
+
ON {table_name} USING GIST ({column_name})
|
|
77
|
+
"""
|
|
78
|
+
self.db.execute_update(query)
|
|
79
|
+
logger.info(f"Índice espacial creado: {index_name}")
|
|
80
|
+
return True
|
|
81
|
+
except Exception as e:
|
|
82
|
+
logger.error(f"Error al crear índice espacial: {e}")
|
|
83
|
+
raise
|
|
84
|
+
|
|
85
|
+
def point_to_geometry(self, lat: float, lon: float, srid: int = 4326) -> str:
|
|
86
|
+
"""
|
|
87
|
+
Convierte coordenadas lat/lon a geometría PostGIS
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
lat: Latitud
|
|
91
|
+
lon: Longitud
|
|
92
|
+
srid: Sistema de referencia espacial (default: 4326 - WGS84)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
String de geometría PostGIS
|
|
96
|
+
"""
|
|
97
|
+
return f"ST_SetSRID(ST_MakePoint({lon}, {lat}), {srid})"
|
|
98
|
+
|
|
99
|
+
def insert_point(
|
|
100
|
+
self,
|
|
101
|
+
table_name: str,
|
|
102
|
+
lat: float,
|
|
103
|
+
lon: float,
|
|
104
|
+
data: Optional[Dict[str, Any]] = None,
|
|
105
|
+
geom_column: str = "geom",
|
|
106
|
+
srid: int = 4326
|
|
107
|
+
) -> int:
|
|
108
|
+
"""
|
|
109
|
+
Inserta un punto geográfico en una tabla
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
table_name: Nombre de la tabla
|
|
113
|
+
lat: Latitud
|
|
114
|
+
lon: Longitud
|
|
115
|
+
data: Datos adicionales a insertar
|
|
116
|
+
geom_column: Nombre de la columna de geometría
|
|
117
|
+
srid: Sistema de referencia espacial
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
ID del registro insertado
|
|
121
|
+
"""
|
|
122
|
+
data = data or {}
|
|
123
|
+
columns = list(data.keys()) + [geom_column]
|
|
124
|
+
placeholders = [f"%s" for _ in data] + [f"ST_SetSRID(ST_MakePoint(%s, %s), {srid})"]
|
|
125
|
+
values = list(data.values()) + [lon, lat]
|
|
126
|
+
|
|
127
|
+
query = f"""
|
|
128
|
+
INSERT INTO {table_name} ({', '.join(columns)})
|
|
129
|
+
VALUES ({', '.join(placeholders)})
|
|
130
|
+
RETURNING id
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
result = self.db.execute_query(query, tuple(values))
|
|
134
|
+
return result[0]['id'] if result else None
|
|
135
|
+
|
|
136
|
+
def find_within_distance(
|
|
137
|
+
self,
|
|
138
|
+
table_name: str,
|
|
139
|
+
lat: float,
|
|
140
|
+
lon: float,
|
|
141
|
+
distance_meters: float,
|
|
142
|
+
geom_column: str = "geom",
|
|
143
|
+
srid: int = 4326
|
|
144
|
+
) -> List[Dict[str, Any]]:
|
|
145
|
+
"""
|
|
146
|
+
Busca puntos dentro de una distancia específica
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
table_name: Nombre de la tabla
|
|
150
|
+
lat: Latitud del punto central
|
|
151
|
+
lon: Longitud del punto central
|
|
152
|
+
distance_meters: Distancia en metros
|
|
153
|
+
geom_column: Nombre de la columna de geometría
|
|
154
|
+
srid: Sistema de referencia espacial
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Lista de registros encontrados
|
|
158
|
+
"""
|
|
159
|
+
query = f"""
|
|
160
|
+
SELECT *,
|
|
161
|
+
ST_Distance(
|
|
162
|
+
{geom_column}::geography,
|
|
163
|
+
ST_SetSRID(ST_MakePoint(%s, %s), {srid})::geography
|
|
164
|
+
) as distance
|
|
165
|
+
FROM {table_name}
|
|
166
|
+
WHERE ST_DWithin(
|
|
167
|
+
{geom_column}::geography,
|
|
168
|
+
ST_SetSRID(ST_MakePoint(%s, %s), {srid})::geography,
|
|
169
|
+
%s
|
|
170
|
+
)
|
|
171
|
+
ORDER BY distance
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
return self.db.execute_query(
|
|
175
|
+
query,
|
|
176
|
+
(lon, lat, lon, lat, distance_meters)
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def find_within_bbox(
|
|
180
|
+
self,
|
|
181
|
+
table_name: str,
|
|
182
|
+
min_lat: float,
|
|
183
|
+
min_lon: float,
|
|
184
|
+
max_lat: float,
|
|
185
|
+
max_lon: float,
|
|
186
|
+
geom_column: str = "geom",
|
|
187
|
+
srid: int = 4326
|
|
188
|
+
) -> List[Dict[str, Any]]:
|
|
189
|
+
"""
|
|
190
|
+
Busca puntos dentro de un bounding box
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
table_name: Nombre de la tabla
|
|
194
|
+
min_lat: Latitud mínima
|
|
195
|
+
min_lon: Longitud mínima
|
|
196
|
+
max_lat: Latitud máxima
|
|
197
|
+
max_lon: Longitud máxima
|
|
198
|
+
geom_column: Nombre de la columna de geometría
|
|
199
|
+
srid: Sistema de referencia espacial
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Lista de registros encontrados
|
|
203
|
+
"""
|
|
204
|
+
query = f"""
|
|
205
|
+
SELECT *
|
|
206
|
+
FROM {table_name}
|
|
207
|
+
WHERE {geom_column} && ST_MakeEnvelope(%s, %s, %s, %s, {srid})
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
return self.db.execute_query(
|
|
211
|
+
query,
|
|
212
|
+
(min_lon, min_lat, max_lon, max_lat)
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def get_coordinates(
|
|
216
|
+
self,
|
|
217
|
+
table_name: str,
|
|
218
|
+
id: int,
|
|
219
|
+
geom_column: str = "geom"
|
|
220
|
+
) -> Optional[Tuple[float, float]]:
|
|
221
|
+
"""
|
|
222
|
+
Obtiene las coordenadas lat/lon de un registro
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
table_name: Nombre de la tabla
|
|
226
|
+
id: ID del registro
|
|
227
|
+
geom_column: Nombre de la columna de geometría
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Tupla (latitud, longitud) o None
|
|
231
|
+
"""
|
|
232
|
+
query = f"""
|
|
233
|
+
SELECT ST_Y({geom_column}) as lat, ST_X({geom_column}) as lon
|
|
234
|
+
FROM {table_name}
|
|
235
|
+
WHERE id = %s
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
result = self.db.execute_query(query, (id,))
|
|
239
|
+
if result:
|
|
240
|
+
return (result[0]['lat'], result[0]['lon'])
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
def calculate_distance(
|
|
244
|
+
self,
|
|
245
|
+
lat1: float,
|
|
246
|
+
lon1: float,
|
|
247
|
+
lat2: float,
|
|
248
|
+
lon2: float,
|
|
249
|
+
srid: int = 4326
|
|
250
|
+
) -> float:
|
|
251
|
+
"""
|
|
252
|
+
Calcula la distancia entre dos puntos en metros
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
lat1: Latitud del punto 1
|
|
256
|
+
lon1: Longitud del punto 1
|
|
257
|
+
lat2: Latitud del punto 2
|
|
258
|
+
lon2: Longitud del punto 2
|
|
259
|
+
srid: Sistema de referencia espacial
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Distancia en metros
|
|
263
|
+
"""
|
|
264
|
+
query = """
|
|
265
|
+
SELECT ST_Distance(
|
|
266
|
+
ST_SetSRID(ST_MakePoint(%s, %s), %s)::geography,
|
|
267
|
+
ST_SetSRID(ST_MakePoint(%s, %s), %s)::geography
|
|
268
|
+
) as distance
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
result = self.db.execute_query(
|
|
272
|
+
query,
|
|
273
|
+
(lon1, lat1, srid, lon2, lat2, srid)
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return result[0]['distance'] if result else 0.0
|
|
277
|
+
|
|
278
|
+
def insert_polygon(
|
|
279
|
+
self,
|
|
280
|
+
table_name: str,
|
|
281
|
+
coordinates: List[Tuple[float, float]],
|
|
282
|
+
data: Optional[Dict[str, Any]] = None,
|
|
283
|
+
geom_column: str = "geom",
|
|
284
|
+
srid: int = 4326
|
|
285
|
+
) -> int:
|
|
286
|
+
"""
|
|
287
|
+
Inserta un polígono en la base de datos
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
table_name: Nombre de la tabla
|
|
291
|
+
coordinates: Lista de tuplas (lat, lon) que forman el polígono
|
|
292
|
+
data: Datos adicionales a insertar
|
|
293
|
+
geom_column: Nombre de la columna de geometría
|
|
294
|
+
srid: Sistema de referencia espacial
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
ID del registro insertado
|
|
298
|
+
"""
|
|
299
|
+
data = data or {}
|
|
300
|
+
|
|
301
|
+
# Convertir coordenadas a formato WKT
|
|
302
|
+
points = ", ".join([f"{lon} {lat}" for lat, lon in coordinates])
|
|
303
|
+
# Cerrar el polígono si no está cerrado
|
|
304
|
+
if coordinates[0] != coordinates[-1]:
|
|
305
|
+
points += f", {coordinates[0][1]} {coordinates[0][0]}"
|
|
306
|
+
|
|
307
|
+
columns = list(data.keys()) + [geom_column]
|
|
308
|
+
placeholders = ["%s" for _ in data] + [f"ST_GeomFromText('POLYGON(({points}))', {srid})"]
|
|
309
|
+
values = list(data.values())
|
|
310
|
+
|
|
311
|
+
query = f"""
|
|
312
|
+
INSERT INTO {table_name} ({', '.join(columns)})
|
|
313
|
+
VALUES ({', '.join(placeholders)})
|
|
314
|
+
RETURNING id
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
result = self.db.execute_query(query, tuple(values) if values else None)
|
|
318
|
+
return result[0]['id'] if result else None
|
|
319
|
+
|
|
320
|
+
def point_in_polygon(
|
|
321
|
+
self,
|
|
322
|
+
table_name: str,
|
|
323
|
+
lat: float,
|
|
324
|
+
lon: float,
|
|
325
|
+
geom_column: str = "geom",
|
|
326
|
+
srid: int = 4326
|
|
327
|
+
) -> List[Dict[str, Any]]:
|
|
328
|
+
"""
|
|
329
|
+
Encuentra polígonos que contienen un punto específico
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
table_name: Nombre de la tabla con polígonos
|
|
333
|
+
lat: Latitud del punto
|
|
334
|
+
lon: Longitud del punto
|
|
335
|
+
geom_column: Nombre de la columna de geometría
|
|
336
|
+
srid: Sistema de referencia espacial
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Lista de polígonos que contienen el punto
|
|
340
|
+
"""
|
|
341
|
+
query = f"""
|
|
342
|
+
SELECT *
|
|
343
|
+
FROM {table_name}
|
|
344
|
+
WHERE ST_Contains(
|
|
345
|
+
{geom_column},
|
|
346
|
+
ST_SetSRID(ST_MakePoint(%s, %s), {srid})
|
|
347
|
+
)
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
return self.db.execute_query(query, (lon, lat))
|