ESCatastroLib 0.0.1rc4__py2.py3-none-any.whl → 1.0.0__py2.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.
@@ -0,0 +1,753 @@
1
+ import requests
2
+ import json
3
+ import xmltodict
4
+ from datetime import datetime
5
+
6
+ from shapely.geometry import Point
7
+ from typing import Union, Dict, Any, Optional, List
8
+ from pyproj import Transformer
9
+
10
+ from ..utils.statics import URL_BASE_CALLEJERO, URL_BASE_GEOGRAFIA, URL_BASE_CROQUIS_DATOS, URL_BASE_MAPA_VALORES_URBANOS, URL_BASE_MAPA_VALORES_RUSTICOS, URL_BASE_WFS_EDIFICIOS, CULTIVOS
11
+ from ..utils.utils import comprobar_errores, listar_sistemas_referencia, lon_lat_from_coords_dict, lat_lon_from_coords_dict, distancia_entre_dos_puntos_geograficos
12
+ from ..utils.exceptions import ErrorServidorCatastro
13
+ from ..utils import converters
14
+ from .Calle import Calle, Municipio
15
+
16
+
17
+ class ParcelaHelper:
18
+ """Clase base con métodos compartidos para operaciones de parcelas catastrales."""
19
+
20
+ def _llamar_a_api(self, url: str, params: Dict[str, Any], timeout: int = 30) -> requests.Response:
21
+ """Realiza una petición HTTP a la API del Catastro.
22
+
23
+ Args:
24
+ url: URL del endpoint a consultar
25
+ params: Parámetros de la petición HTTP
26
+ timeout: Tiempo máximo de espera en segundos
27
+
28
+ Returns:
29
+ Objeto Response con la respuesta del servidor
30
+
31
+ Raises:
32
+ ErrorServidorCatastro: Si el servidor devuelve una respuesta vacía o error
33
+ """
34
+ response = requests.get(url, params=params, timeout=timeout)
35
+
36
+ if len(response.content) == 0:
37
+ raise ErrorServidorCatastro("El servidor ha devuelto una respuesta vacía")
38
+
39
+ return response
40
+
41
+ def _parsear_respuesta(self, response: requests.Response) -> Dict[str, Any]:
42
+ """Parsea la respuesta JSON de la API del Catastro.
43
+
44
+ Args:
45
+ response: Objeto Response de la petición HTTP
46
+
47
+ Returns:
48
+ Diccionario con los datos parseados
49
+
50
+ Raises:
51
+ ErrorServidorCatastro: Si el servidor no devuelve un JSON válido
52
+ """
53
+ try:
54
+ return response.json()
55
+ except json.JSONDecodeError:
56
+ raise ErrorServidorCatastro(
57
+ mensaje=f"El servidor no devuelve un JSON. Mensaje en bruto: {response.content}"
58
+ )
59
+
60
+ def _parametrizar_peticion(self, **kwargs) -> Dict[str, Any]:
61
+ """Construye un diccionario de parámetros para la petición HTTP.
62
+
63
+ Args:
64
+ **kwargs: Parámetros clave-valor para la petición
65
+
66
+ Returns:
67
+ Diccionario con los parámetros filtrados (solo valores no nulos)
68
+ """
69
+ return {k: v for k, v in kwargs.items() if v is not None}
70
+
71
+ def _comprobar_errores_catastro(self, info_cadastre: Dict[str, Any]) -> bool:
72
+ """Verifica si la respuesta del Catastro contiene errores.
73
+
74
+ Args:
75
+ info_cadastre: Diccionario con la respuesta del Catastro
76
+
77
+ Returns:
78
+ True si no hay errores, False si hay errores
79
+ """
80
+ return comprobar_errores(info_cadastre)
81
+
82
+ def _obtener_numero_parcelas(self, info_cadastre: Dict[str, Any], consulta_key: str) -> int:
83
+ """Obtiene el número de parcelas de una respuesta del Catastro.
84
+
85
+ Args:
86
+ info_cadastre: Diccionario con la respuesta del Catastro
87
+ consulta_key: Clave de la consulta ('dnprc', 'dnppp', 'dnploc')
88
+
89
+ Returns:
90
+ Número de parcelas encontradas
91
+ """
92
+ result_key = f"consulta_{consulta_key}Result"
93
+ return info_cadastre.get(result_key, {}).get("control", {}).get("cudnp", 1)
94
+
95
+ def _extraer_rc_from_dict(self, rc_dict: Dict[str, Any]) -> str:
96
+ """Extrae la referencia catastral de un diccionario.
97
+
98
+ Args:
99
+ rc_dict: Diccionario que contiene la referencia catastral
100
+
101
+ Returns:
102
+ Referencia catastral como string
103
+ """
104
+ return ''.join(rc_dict.values())
105
+
106
+
107
+ class ParcelaCatastral(ParcelaHelper):
108
+ """
109
+ Clase que representa una parcela catastral.
110
+ Args:
111
+ rc (str, optional): La referencia catastral de la parcela. Defaults to None. Puede ir solo.
112
+
113
+ provincia (int|str, optional): El código o nombre de la provincia. Defaults to None. Se usa para buscar por dirección o parcela.
114
+ municipio (int|str, optional): El código o nombre del municipio. Defaults to None. Se usa para buscar por dirección o parcela.
115
+ poligono (int, optional): El número del polígono. Defaults to None. Se usa para buscar por parcela.
116
+ parcela (int, optional): El número de la parcela. Defaults to None. Se usa para buscar por parcela.
117
+ tipo_via (str, optional): El tipo de vía de la dirección. Defaults to None. Se usa para buscar por dirección.
118
+ calle (str, optional): El nombre de la calle de la dirección. Defaults to None. Se usa para buscar por dirección.
119
+ numero (str, optional): El número de la dirección. Defaults to None. Se usa para buscar por dirección.
120
+ Raises:
121
+ ValueError: Se lanza si no se proporciona suficiente información para realizar la búsqueda o si la RC corresponde a una MetaParcela.
122
+ ErrorServidorCatastro: Se lanza si hay un error en el servidor del Catastro.
123
+ Attributes:
124
+ rc (str): La referencia catastral de la parcela.
125
+ provincia (int|str): El código o nombre de la provincia.
126
+ municipio (int|str): El código o nombre del municipio.
127
+ poligono (int): El número del polígono. Sólo se da en terrenos Rústicos.
128
+ parcela (int): El número de la parcela. Sólo se da en terrenos Rústicos.
129
+ tipo_via (str): El tipo de vía de la dirección. Sólo se da en terrenos Urbanos.
130
+ calle (str): El nombre de la calle de la dirección. Sólo se da en terrenos Urbanos.
131
+ numero (str): El número de la dirección. Sólo se da en terrenos Urbanos.
132
+ url_croquis (str): La URL del croquis de la parcela.
133
+ tipo (str): El tipo de la parcela (Urbano o Rústico).
134
+ antiguedad (str): La antigüedad de la parcela (solo para parcelas urbanas).
135
+ uso (str): El uso de la parcela (solo para parcelas urbanas).
136
+ nombre_paraje (str): El nombre del paraje (solo para parcelas rústicas).
137
+ regiones (list): Una lista de regiones de la parcela, cada una con una descripción y superficie.
138
+ centroide (dict): Las coordenadas del centroide de la parcela.
139
+ geometria (list): Una lista de puntos que representan la geometría de la parcela.
140
+ """
141
+
142
+ def __create_regions(self, info_cadastre: Dict[str, Any]) -> None:
143
+ """Crea la lista de regiones de la parcela a partir de los datos del Catastro.
144
+
145
+ Args:
146
+ info_cadastre: Diccionario con la respuesta del Catastro.
147
+
148
+ Attributes:
149
+ regiones: Lista de diccionarios con 'descripcion' y 'superficie' de cada región.
150
+ """
151
+ self.regiones = []
152
+ if self.tipo == 'Urbano':
153
+ iterator = list(info_cadastre.values())[0].get('bico').get('lcons')
154
+ elif self.tipo == 'Rústico':
155
+ iterator = list(info_cadastre.values())[0].get('bico').get('lspr')
156
+ for region in iterator:
157
+ if self.tipo == 'Rústico':
158
+ self.regiones.append({
159
+ 'descripcion': region.get('dspr').get('dcc'),
160
+ 'superficie': region.get('dspr').get('ssp')
161
+ })
162
+ elif self.tipo == 'Urbano':
163
+ self.regiones.append({
164
+ 'descripcion': region.get('lcd'),
165
+ 'superficie': region.get('dfcons').get('stl')
166
+ })
167
+
168
+
169
+ def __create_geometry(self, projection: str = 'EPSG:4326') -> None:
170
+ """Crea la geometría de la parcela consultando el servicio WFS.
171
+
172
+ Args:
173
+ projection: Sistema de referencia espacial (EPSG). Por defecto 'EPSG:4326'.
174
+
175
+ Raises:
176
+ ErrorServidorCatastro: Si el servidor devuelve una respuesta vacía o error.
177
+ """
178
+ params = self._parametrizar_peticion(
179
+ service='wfs',
180
+ version='2',
181
+ request='getfeature',
182
+ STOREDQUERIE_ID='GetParcel',
183
+ refcat=self.rc,
184
+ srsname=projection
185
+ )
186
+ response = self._llamar_a_api(f'{URL_BASE_GEOGRAFIA}', params)
187
+
188
+ geometry = xmltodict.parse(response.content)
189
+ geoposition = geometry.get('FeatureCollection').get('member').get('cp:CadastralParcel').get('cp:referencePoint').get('gml:Point').get('gml:pos').split(' ')
190
+ self.centroide = {
191
+ 'x': geoposition[1],
192
+ 'y': geoposition[0]
193
+ }
194
+ parcel_geometry = geometry.get('FeatureCollection').get('member').get('cp:CadastralParcel').get('cp:geometry').get('gml:MultiSurface').get('gml:surfaceMember').get('gml:Surface').get('gml:patches').get('gml:PolygonPatch').get('gml:exterior').get('gml:LinearRing').get('gml:posList').get('#text').split(' ')
195
+ self.geometria = [
196
+ {
197
+ 'x': parcel_geometry[2*idx+1],
198
+ 'y': parcel_geometry[2*idx]
199
+ } for idx in range(len(parcel_geometry)//2)
200
+ ]
201
+ self.superficie_total = float(geometry.get('FeatureCollection').get('member').get('cp:CadastralParcel').get('cp:areaValue').get('#text'))
202
+
203
+ def __create_from_rc(self, rc: str, projection: str):
204
+ """Create an instance of InfoCatastral from a RC (Referencia Catastral) string."""
205
+ params = self._parametrizar_peticion(RefCat=rc)
206
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPRC', params)
207
+ info_cadastre = self._parsear_respuesta(response)
208
+
209
+ if self._comprobar_errores_catastro(info_cadastre):
210
+ cudnp = self._obtener_numero_parcelas(info_cadastre, 'dnprc')
211
+
212
+ if cudnp > 1:
213
+ raise ErrorServidorCatastro(mensaje="Esta parcela tiene varias referencias catastrales. Usa un objeto MetaParcela.")
214
+ else:
215
+ self.rc = self._extraer_rc_from_dict(info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('idbi').get('rc'))
216
+ self.url_croquis = requests.get(URL_BASE_CROQUIS_DATOS, params={'refcat': self.rc}).url
217
+ self.municipio = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('nm')
218
+ self.provincia = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('np')
219
+ self.tipo = 'Rústico' if info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('idbi').get('cn') == 'RU' else 'Urbano'
220
+ if self.tipo == 'Urbano':
221
+ self.calle = f"{info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lous').get('lourb').get('dir').get('tv')} {info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lous').get('lourb').get('dir').get('nv')}"
222
+ self.numero = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lous').get('lourb').get('dir').get('pnp')
223
+ self.antiguedad = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('debi').get('ant')
224
+ self.uso = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('debi').get('luso')
225
+ elif self.tipo == 'Rústico':
226
+ self.parcela = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lors').get('lorus').get('cpp').get('cpa')
227
+ self.poligono = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lors').get('lorus').get('cpp').get('cpo')
228
+ self.nombre_paraje = info_cadastre.get('consulta_dnprcResult').get('bico').get('bi').get('dt').get('locs').get('lors').get('lorus').get('npa')
229
+
230
+ self.__create_regions(info_cadastre)
231
+ self.__create_geometry(projection)
232
+
233
+ self.superficie_construida = sum(float(region.get('superficie')) for region in self.regiones)
234
+ self.superficie = sum(float(region.get('superficie')) for region in self.regiones)
235
+
236
+ def __create_from_parcel(self, provincia: Union[str,None], municipio: Union[str,None], poligono: Union[str,None], parcela: Union[str,None], projection: str):
237
+ """Create an instance of InfoCatastral from a parcela string."""
238
+ params = self._parametrizar_peticion(
239
+ Provincia=provincia,
240
+ Municipio=municipio,
241
+ Poligono=poligono,
242
+ Parcela=parcela
243
+ )
244
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPPP', params)
245
+ info_cadastre = self._parsear_respuesta(response)
246
+
247
+ if self._comprobar_errores_catastro(info_cadastre):
248
+ cudnp = self._obtener_numero_parcelas(info_cadastre, 'dnppp')
249
+
250
+ if cudnp > 1:
251
+ raise ErrorServidorCatastro(mensaje="Esta parcela tiene varias referencias catastrales. Usa un objeto MetaParcela.")
252
+ else:
253
+ self.rc = self._extraer_rc_from_dict(info_cadastre.get('consulta_dnpppResult').get('bico').get('bi').get('idbi').get('rc'))
254
+ self.__create_from_rc(self.rc, projection)
255
+
256
+ def __create_from_address(self, provincia: Union[str,None], municipio: Union[str,None], tipo_via: Union[str,None], calle: Union[str,None], numero: Union[str,None], projection: str):
257
+ """Create an instance of InfoCatastral from an address string."""
258
+ info_calle = Calle(
259
+ municipio=Municipio(
260
+ provincia=provincia,
261
+ municipio=municipio
262
+ ),
263
+ tipo_via=tipo_via,
264
+ nombre_calle=calle
265
+ )
266
+
267
+ if info_calle:
268
+ params = self._parametrizar_peticion(
269
+ Provincia=info_calle.municipio.provincia,
270
+ Municipio=info_calle.municipio.municipio,
271
+ Sigla=info_calle.tipo_via,
272
+ Calle=info_calle.calle,
273
+ Numero=numero
274
+ )
275
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPLOC', params)
276
+ info_cadastre = self._parsear_respuesta(response)
277
+
278
+ if self._comprobar_errores_catastro(info_cadastre):
279
+ cudnp = self._obtener_numero_parcelas(info_cadastre, 'dnploc')
280
+
281
+ if cudnp > 1:
282
+ raise ErrorServidorCatastro(mensaje="Esta parcela tiene varias referencias catastrales. Usa un objeto MetaParcela.")
283
+ else:
284
+ if 'lrcdnp' in info_cadastre.get('consulta_dnplocResult'):
285
+ self.rc = self._extraer_rc_from_dict(info_cadastre.get('consulta_dnplocResult').get('lrcdnp').get('rcdnp')[0].get('rc'))
286
+ elif 'bico' in info_cadastre.get('consulta_dnplocResult'):
287
+ self.rc = self._extraer_rc_from_dict(info_cadastre.get('consulta_dnplocResult').get('bico').get('bi').get('idbi').get('rc'))
288
+ self.__create_from_rc(self.rc, projection)
289
+ elif 'lerr' in info_cadastre.get('consulta_dnplocResult') and info_cadastre['consulta_dnplocResult']['lerr'][0]['cod'] == '43':
290
+ raise Exception(f"Ese número no existe. Prueba con alguno de estos: {[num.get('num').get('pnp') for num in info_cadastre.get('consulta_dnplocResult').get('numerero').get('nump')]}")
291
+ else:
292
+ raise ErrorServidorCatastro("El servidor ha devuelto una respuesta vacia")
293
+ else:
294
+ raise Exception('La calle no existe.')
295
+
296
+ def __init__(self, rc: Union[str,None] = None, provincia: Union[str,None] = None, municipio: Union[int,str,None] = None, poligono: Union[int,None] = None, parcela: Union[int,None] = None, tipo_via: Union[str,None] = None, calle: Union[str,None] = None, numero: Union[str,None] = None, projection: str = 'EPSG:4326'):
297
+ if projection not in listar_sistemas_referencia():
298
+ raise ValueError(f"El sistema de referencia {projection} no existe. Los sistemas de referencia disponibles son: {listar_sistemas_referencia()}")
299
+ if rc:
300
+ self.rc = rc
301
+ self.__create_from_rc(rc, projection)
302
+ elif provincia and municipio and poligono and parcela:
303
+ self.provincia = provincia
304
+ self.municipio = municipio
305
+ self.poligono = poligono
306
+ self.parcela = parcela
307
+ self.__create_from_parcel(provincia, municipio, poligono, parcela, projection)
308
+ elif provincia and municipio and tipo_via and calle and numero:
309
+ self.provincia = provincia
310
+ self.municipio = municipio
311
+ self.calle = calle
312
+ self.numero = numero
313
+ self.__create_from_address(provincia, municipio, tipo_via, calle, numero, projection)
314
+ else:
315
+ raise ValueError("No se ha proporcionado suficiente información para realizar la búsqueda")
316
+
317
+ @property
318
+ def distancias_aristas(self) -> Optional[List[float]]:
319
+ """Calcula las distancias entre puntos consecutivos de la geometría de la parcela.
320
+
321
+ Returns:
322
+ Lista de distancias entre aristas consecutivas en metros, o None si no hay geometría.
323
+ """
324
+ if self.geometria:
325
+ distancias = []
326
+ for idx in range(0, len(self.geometria)):
327
+ idx_0 = len(self.geometria)-1 if idx == 0 else idx-1
328
+ idx_f=idx
329
+ distancias.append(distancia_entre_dos_puntos_geograficos(
330
+ lat_lon_from_coords_dict(self.geometria[idx_0]),
331
+ lat_lon_from_coords_dict(self.geometria[idx_f])
332
+ ))
333
+ return distancias
334
+ else:
335
+ return None
336
+
337
+ @property
338
+ def perimetro(self) -> Optional[float]:
339
+ """Calcula el perímetro total de la geometría de la parcela.
340
+
341
+ Returns:
342
+ Perímetro total en metros, o None si no hay geometría.
343
+ """
344
+ distancias = self.distancias_aristas
345
+ if distancias:
346
+ return sum(distancias)
347
+ else:
348
+ return None
349
+
350
+ def valor_catastral_urbano_m2(self, anio: int) -> Optional[float]:
351
+ """Obtiene el valor catastral por metro cuadrado para parcelas urbanas.
352
+
353
+ Args:
354
+ anio: Año del valor catastral a consultar.
355
+
356
+ Returns:
357
+ Valor catastral en €/m², 0 si es rústica, o None si hay algún error.
358
+ """
359
+ if self.tipo == 'Rústico':
360
+ return 0
361
+
362
+ params = self._parametrizar_peticion(
363
+ huso="4326",
364
+ x=self.centroide['x'],
365
+ y=self.centroide['y'],
366
+ anyoZV=anio,
367
+ suelo="N",
368
+ tipo_mapa="vivienda"
369
+ )
370
+ req = self._llamar_a_api(f'{URL_BASE_MAPA_VALORES_URBANOS}', params)
371
+
372
+ values_map = converters.gpd.read_file(req.content)
373
+ centroide_point = Point(self.centroide['x'],self.centroide['y'])
374
+ selected_polygon = values_map[values_map.geometry.covers(centroide_point)]
375
+
376
+ return selected_polygon['Ptipo1'].iloc[0].get('val_tipo_m2')
377
+
378
+ @property
379
+ def numero_plantas(self) -> Dict[str, Any]:
380
+ """Obtiene el número de plantas de un edificio a partir de su referencia catastral.
381
+
382
+ Returns:
383
+ Diccionario con:
384
+ - 'plantas': Máximo número de plantas sobre rasante (o None)
385
+ - 'sotanos': Máximo número de plantas bajo rasante
386
+ - 'total': Suma de plantas sobre y bajo rasante (o None)
387
+ """
388
+ if self.tipo == 'Rústico':
389
+ return 0
390
+
391
+ params = {
392
+ "service": "WFS",
393
+ "version": "2.0.0",
394
+ "request": "GetFeature",
395
+ "STOREDQUERIE_ID": "GetBuildingPartByParcel",
396
+ "REFCAT": self.rc,
397
+ "srsname": "EPSG:4326"
398
+ }
399
+
400
+ response = requests.get(URL_BASE_WFS_EDIFICIOS, params=params)
401
+ response.raise_for_status()
402
+
403
+ data = xmltodict.parse(response.content)
404
+
405
+ feature_collection = data.get("gml:FeatureCollection", {})
406
+ members = feature_collection.get("gml:featureMember", [])
407
+
408
+ if isinstance(members, dict):
409
+ members = [members]
410
+
411
+ above_floors = []
412
+ below_floors = []
413
+ parts_detail = []
414
+
415
+ for member in members:
416
+ bp = member.get("bu-ext2d:BuildingPart")
417
+ if not bp:
418
+ continue
419
+
420
+ part_id = bp.get("@gml:id")
421
+
422
+ above = bp.get("bu-ext2d:numberOfFloorsAboveGround")
423
+ below = bp.get("bu-ext2d:numberOfFloorsBelowGround")
424
+
425
+ above_i = int(above) if above is not None else None
426
+ below_i = int(below) if below is not None else 0
427
+
428
+ if above_i is not None:
429
+ above_floors.append(above_i)
430
+ below_floors.append(below_i)
431
+
432
+ parts_detail.append({
433
+ "id": part_id,
434
+ "floors_above_ground": above_i,
435
+ "floors_below_ground": below_i
436
+ })
437
+
438
+ return {
439
+ "plantas": max(above_floors) if above_floors else None,
440
+ "sotanos": max(below_floors) if below_floors else 0,
441
+ "total": (
442
+ max(above_floors) + max(below_floors)
443
+ if above_floors else None
444
+ )
445
+ }
446
+
447
+ def valor_catastral_rustico_m2(self, anio:str):
448
+ """
449
+ Obtiene los valores catastrales de tierras a partir de una referencia catastral.
450
+
451
+ Args:
452
+ referencia_catastral (str): Referencia catastral de la parcela.
453
+
454
+ Returns:
455
+ dict: Datos de la parcela con región y módulos €/ha, o None si no se encuentran.
456
+ """
457
+
458
+ if self.tipo == "Urbano":
459
+ return {}
460
+
461
+ geometria = self.geometria
462
+
463
+ # Transformar geometría EPSG:4381 → EPSG:3857
464
+ transformer = Transformer.from_crs("EPSG:4381", "EPSG:3857", always_xy=True)
465
+ xs_3857 = []
466
+ ys_3857 = []
467
+ for p in geometria:
468
+ x, y = transformer.transform(p["x"], p["y"])
469
+ xs_3857.append(x)
470
+ ys_3857.append(y)
471
+
472
+ # BBOX EPSG:3857
473
+ bbox = f"{min(xs_3857)},{min(ys_3857)},{max(xs_3857)},{max(ys_3857)}"
474
+
475
+ params = {
476
+ "SERVICE": "WMS",
477
+ "VERSION": "1.3.0",
478
+ "REQUEST": "GetFeatureInfo",
479
+ "LAYERS": f"IAMIR{int(str(anio)[-2:])-1}:athiamir{int(str(anio)[-2:])-1}",
480
+ "QUERY_LAYERS": f"IAMIR{int(str(anio)[-2:])-1}:athiamir{int(str(anio)[-2:])-1}",
481
+ "STYLES": "",
482
+ "CRS": "EPSG:3857",
483
+ "SRS": "EPSG:3857",
484
+ "BBOX": bbox,
485
+ "WIDTH": 101,
486
+ "HEIGHT": 101,
487
+ "FORMAT": "image/png",
488
+ "TRANSPARENT": "true",
489
+ "I": 55,
490
+ "J": 55,
491
+ "INFO_FORMAT": "application/json"
492
+ }
493
+
494
+ r = requests.get(URL_BASE_MAPA_VALORES_RUSTICOS, params=params, timeout=15)
495
+ r.raise_for_status()
496
+ data = r.json()
497
+
498
+ if not data.get("features"):
499
+ return None
500
+
501
+ props = data["features"][0]["properties"]
502
+
503
+ modulos = {
504
+ "region": props.get("REGIONAL"),
505
+ "nombre_region": props.get("NOMBRE"),
506
+ "modulos_€/ha": {}
507
+ }
508
+
509
+ for cod, desc in CULTIVOS.items():
510
+ val = props.get(cod)
511
+ if isinstance(val, (int, float)) and val > 0:
512
+ modulos["modulos_€/ha"][desc] = val
513
+
514
+ return {
515
+ "region": modulos["region"],
516
+ "nombre_region": modulos["nombre_region"],
517
+ "modulos_€/ha": modulos["modulos_€/ha"]
518
+ }
519
+
520
+
521
+ def to_dataframe(self):
522
+ """
523
+ Convierte la parcela en un DataFrame de pandas.
524
+
525
+ Returns:
526
+ pd.DataFrame: Un DataFrame que contiene los datos de la parcela.
527
+ """
528
+ return converters.to_geodataframe([self])
529
+
530
+ def to_json(self, filename: Union[str,None] = None) -> str:
531
+ """
532
+ Convierte la parcela en un JSON.
533
+
534
+ Args:
535
+ filename (Union[str,None], optional): Nombre del archivo donde guardar el JSON. Defaults to None.
536
+
537
+ Returns:
538
+ str: Una cadena JSON que contiene los datos de la parcela.
539
+ """
540
+ return converters.to_json([self], filename)
541
+
542
+ def to_csv(self, filename: Union[str,None] = None) -> str:
543
+ """
544
+ Convierte la parcela en un CSV.
545
+
546
+ Args:
547
+ filename (Union[str,None], optional): Nombre del archivo donde guardar el CSV. Defaults to None.
548
+
549
+ Returns:
550
+ str: Una cadena CSV que contiene los datos de la parcela.
551
+ """
552
+ return converters.to_csv([self], filename)
553
+
554
+ def to_shapefile(self, filename: str):
555
+ """
556
+ Guarda la parcela como un archivo Shapefile.
557
+
558
+ Args:
559
+ filename (str): El nombre del archivo Shapefile a guardar.
560
+ """
561
+ converters.to_shapefile([self], filename)
562
+
563
+ def to_parquet(self, filename: str):
564
+ """
565
+ Guarda la parcela como un archivo Parquet.
566
+
567
+ Args:
568
+ filename (str): El nombre del archivo Parquet a guardar.
569
+ """
570
+ converters.to_parquet([self], filename)
571
+
572
+ class MetaParcela(ParcelaHelper):
573
+ """
574
+ Clase que representa una MetaParcela, es decir, una gran parcela catastral con
575
+ varias referencias catastrales (Parcelas Catastrales más pequeñas).
576
+
577
+ Args:
578
+ rc (Union[str,None]): La referencia catastral de la MetaParcela.
579
+
580
+ provincia (int|Union[str,None]): El nombre de la provincia donde se encuentra la MetaParcela.
581
+ municipio (int|Union[str,None]): El nombre del municipio donde se encuentra la MetaParcela.
582
+ poligono (Union[int,None]): El número de polígono de la MetaParcela. Sólo se usa para buscar por parcela.
583
+ parcela (Union[int,None]): El número de parcela de la MetaParcela. Sólo se usa para buscar por parcela.
584
+ tipo_via (Union[str,None]): El tipo de vía de la dirección de la MetaParcela. Sólo se usa para buscar por dirección.
585
+ calle (Union[str,None]): El nombre de la calle de la dirección de la MetaParcela. Sólo se usa para buscar por dirección.
586
+ numero (Union[str,None]): El número de la dirección de la MetaParcela. Sólo se usa para buscar por dirección.
587
+ Attributes:
588
+ rc (str): La referencia catastral de la MetaParcela.
589
+ parcelas (list): Una lista de ParcelaCatastral que representan las parcelas que componen la MetaParcela.
590
+
591
+ """
592
+
593
+ def __create_from_rc(self, rc: str) -> None:
594
+ """Create an instance of MetaParcela from a RC (Referencia Catastral) string.
595
+
596
+ Args:
597
+ rc: Referencia catastral de la metaparcela
598
+
599
+ Raises:
600
+ ErrorServidorCatastro: Si el servidor devuelve una respuesta vacía o error
601
+ """
602
+ params = self._parametrizar_peticion(RefCat=rc)
603
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPRC', params)
604
+ info_cadastre = self._parsear_respuesta(response)
605
+
606
+ if self._comprobar_errores_catastro(info_cadastre):
607
+ self.parcelas = []
608
+ num_parcelas = self._obtener_numero_parcelas(info_cadastre, 'dnprc')
609
+ for idx in range(num_parcelas):
610
+ rc_dict = info_cadastre.get('consulta_dnprcResult').get('lrcdnp').get('rcdnp')[idx].get('rc')
611
+ rc = self._extraer_rc_from_dict(rc_dict)
612
+ self.parcelas.append(ParcelaCatastral(rc=rc))
613
+
614
+
615
+ def __create_from_parcel(self, provincia: Union[str,None], municipio: Union[str,None], poligono: Union[str,None], parcela: Union[str,None]) -> None:
616
+ """Create an instance of MetaParcela from parcel data.
617
+
618
+ Args:
619
+ provincia: Provincia donde se encuentra la parcela
620
+ municipio: Municipio donde se encuentra la parcela
621
+ poligono: Número de polígono
622
+ parcela: Número de parcela
623
+
624
+ Raises:
625
+ ErrorServidorCatastro: Si el servidor devuelve una respuesta vacía o error
626
+ """
627
+ params = self._parametrizar_peticion(
628
+ Provincia=provincia,
629
+ Municipio=municipio,
630
+ Poligono=poligono,
631
+ Parcela=parcela
632
+ )
633
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPPP', params)
634
+ info_cadastre = self._parsear_respuesta(response)
635
+
636
+ if self._comprobar_errores_catastro(info_cadastre):
637
+ self.parcelas = []
638
+ num_parcelas = self._obtener_numero_parcelas(info_cadastre, 'dnppp')
639
+ for idx in range(num_parcelas):
640
+ rc_dict = info_cadastre.get('consulta_dnpppResult').get('lrcdnp').get('rcdnp')[idx].get('rc')
641
+ rc = self._extraer_rc_from_dict(rc_dict)
642
+ self.parcelas.append(ParcelaCatastral(rc=rc))
643
+
644
+ def __create_from_address(self, provincia: Union[str,None], municipio: Union[str,None], tipo_via: Union[str,None], calle: Union[str,None], numero: Union[str,None]) -> None:
645
+ """Create an instance of MetaParcela from an address.
646
+
647
+ Args:
648
+ provincia: Provincia donde se encuentra la dirección
649
+ municipio: Municipio donde se encuentra la dirección
650
+ tipo_via: Tipo de vía (calle, avenida, etc.)
651
+ calle: Nombre de la calle
652
+ numero: Número de la dirección
653
+
654
+ Raises:
655
+ ErrorServidorCatastro: Si el servidor devuelve una respuesta vacía o error
656
+ Exception: Si la calle no existe
657
+ """
658
+ info_calle = Calle(
659
+ municipio=Municipio(
660
+ provincia=provincia,
661
+ municipio=municipio
662
+ ),
663
+ tipo_via=tipo_via,
664
+ nombre_calle=calle
665
+ )
666
+
667
+ if info_calle:
668
+ params = self._parametrizar_peticion(
669
+ Provincia=info_calle.municipio.provincia,
670
+ Municipio=info_calle.municipio.municipio,
671
+ Sigla=info_calle.tipo_via,
672
+ Calle=info_calle.calle,
673
+ Numero=numero
674
+ )
675
+ response = self._llamar_a_api(f'{URL_BASE_CALLEJERO}/Consulta_DNPLOC', params)
676
+ info_cadastre = self._parsear_respuesta(response)
677
+
678
+ if self._comprobar_errores_catastro(info_cadastre):
679
+ self.parcelas = []
680
+ num_parcelas = self._obtener_numero_parcelas(info_cadastre, 'dnploc')
681
+ for idx in range(num_parcelas):
682
+ rc_dict = info_cadastre.get('consulta_dnplocResult').get('lrcdnp').get('rcdnp')[idx].get('rc')
683
+ rc = self._extraer_rc_from_dict(rc_dict)
684
+ self.parcelas.append(ParcelaCatastral(rc=rc))
685
+ else:
686
+ raise Exception('La calle no existe.')
687
+
688
+ def __init__(self, rc: Union[str,None] = None, provincia: Union[int,str,None] = None, municipio: Union[int,str,None] = None, poligono: Union[int,None] = None, parcela: Union[int,None] = None, tipo_via: Union[str,None] = None, calle: Union[str,None] = None, numero: Union[str,None] = None) -> None:
689
+ if rc:
690
+ self.rc = rc
691
+ self.__create_from_rc(rc)
692
+ elif provincia and municipio and poligono and parcela:
693
+ self.provincia = provincia
694
+ self.municipio = municipio
695
+ self.poligono = poligono
696
+ self.parcela = parcela
697
+ self.__create_from_parcel(provincia, municipio, poligono, parcela)
698
+ elif provincia and municipio and tipo_via and calle and numero:
699
+ self.provincia = provincia
700
+ self.municipio = municipio
701
+ self.calle = calle
702
+ self.numero = numero
703
+ self.__create_from_address(provincia, municipio, tipo_via, calle, numero)
704
+ else:
705
+ raise ValueError("No se ha proporcionado suficiente información para realizar la búsqueda")
706
+
707
+
708
+ def to_dataframe(self):
709
+ """Convierte la MetaParcela en un DataFrame de pandas.
710
+
711
+ Returns:
712
+ pd.DataFrame: Un DataFrame que contiene las parcelas de la MetaParcela.
713
+ """
714
+ return converters.to_geodataframe(self.parcelas)
715
+
716
+ def to_json(self, filename: Union[str,None] = None) -> str:
717
+ """Convierte la MetaParcela en un JSON.
718
+
719
+ Args:
720
+ filename: Nombre del archivo donde guardar el JSON. Defaults to None.
721
+
722
+ Returns:
723
+ str: Una cadena JSON que contiene las parcelas de la MetaParcela.
724
+ """
725
+ return converters.to_json(self.parcelas, filename)
726
+
727
+ def to_csv(self, filename: Union[str,None] = None) -> str:
728
+ """Convierte la MetaParcela en un CSV.
729
+
730
+ Args:
731
+ filename: Nombre del archivo donde guardar el CSV. Defaults to None.
732
+
733
+ Returns:
734
+ str: Una cadena CSV que contiene las parcelas de la MetaParcela.
735
+ """
736
+ return converters.to_csv(self.parcelas, filename)
737
+
738
+ def to_shapefile(self, filename: str) -> None:
739
+ """Guarda la MetaParcela como un archivo Shapefile.
740
+
741
+ Args:
742
+ filename: El nombre del archivo Shapefile a guardar.
743
+ """
744
+ converters.to_shapefile(self.parcelas, filename)
745
+
746
+ def to_parquet(self, filename: str) -> None:
747
+ """Guarda la MetaParcela como un archivo Parquet.
748
+
749
+ Args:
750
+ filename: El nombre del archivo Parquet a guardar.
751
+ """
752
+ converters.to_parquet(self.parcelas, filename)
753
+