apb_cx_oracle_spatial.sdo_geom
1# coding=utf-8 2# # 3# Author: Ernesto Arredondo Martinez (ernestone@gmail.com) 4# File: sdo_geom.py 5# Created: 05/04/2020, 17:47 6# Last modified: 10/11/2019, 11:24 7# Copyright (c) 2020 8import json 9import re 10import sys 11 12import cx_Oracle 13from math import atan2, degrees 14from osgeo import ogr 15from shapely.geometry import shape 16 17from apb_extra_utils.misc import rounded_float 18import apb_extra_osgeo_utils 19from apb_spatial_utils import shapely_utils 20 21 22class sdo_geom(object): 23 """ 24 Clase que instanciará objectos MDSYS.SDO_GEOMETRY devueltos por cx_Oracle añadiendo funcionalidad para convertir a 25 distintos formatos geométricos. Se representará como una geometria geojson a partir de interfaz _geo_interface_ 26 """ 27 __slots__ = ["__SDO_GTYPE", 28 "__SDO_SRID", 29 "__SDO_POINT", 30 "__SDO_ELEM_INFO", 31 "__SDO_ORDINATES", 32 "__SDO_DIM", 33 "__SDO_LRS", 34 "__DD_SDO_GTYPE", 35 "__type_geojson", 36 "__idxs_elems_geom", 37 "__con_db", 38 "__ora_sdo_geometry", 39 "__shapely_obj", 40 "__ogr_geom", 41 "__cache_transformed_geoms", 42 "__cache_formatted_geoms"] 43 44 __ora_dd_gtypes = {0: "UNKNOWN_GEOMETRY", 45 1: "POINT", 46 2: "LINE", # "CURVE", 47 3: "POLYGON", # "SURFACE", 48 4: "COLLECTION", 49 5: "MULTIPOINT", 50 6: "MULTILINE", # "MULTICURVE", 51 7: "MULTIPOLYGON", # "MULTISURFACE", 52 8: "SOLID", 53 9: "MULTISOLID"} 54 55 __ora_gtypes_geojson_types = {1: "Point", 56 5: "MultiPoint", 57 2: "LineString", 58 6: "MultiLineString", 59 3: "Polygon", 60 7: "MultiPolygon", 61 4: "GeometryCollection"} 62 63 def __init__(self, cx_ora_sdo_geom, con_ora_db): 64 """ 65 Genera instancia para geometria Oracle MDSYS.SDO_GEOMETRY 66 67 Args: 68 cx_ora_sdo_geom: valor MDSYS.SDO_GEOMETRY a partir de cx_Oracle 69 con_ora_db: conexion Oracle cx_Oracle.Connection 70 """ 71 self.__SDO_GTYPE = int(cx_ora_sdo_geom.SDO_GTYPE) 72 self.__SDO_SRID = int(cx_ora_sdo_geom.SDO_SRID) 73 74 sdo_point_orig = cx_ora_sdo_geom.SDO_POINT 75 a_sdo_point = None 76 l_elem_info = None 77 l_ordinates = None 78 if sdo_point_orig: 79 a_sdo_point = sdo_point_orig.copy() 80 else: 81 l_elem_info = cx_ora_sdo_geom.SDO_ELEM_INFO.aslist() 82 l_ordinates = cx_ora_sdo_geom.SDO_ORDINATES.aslist() 83 84 self.__SDO_POINT = a_sdo_point 85 self.__SDO_ELEM_INFO = l_elem_info 86 self.__SDO_ORDINATES = l_ordinates 87 88 self.__con_db = con_ora_db 89 90 self.__idxs_elems_geom = [] 91 92 self._parse_oracle_params() 93 94 self.__ora_sdo_geometry = None 95 self.__shapely_obj = None 96 self.__ogr_geom = None 97 self.__cache_transformed_geoms = {} 98 self.__cache_formatted_geoms = {} 99 100 def _gen_elems_info(self): 101 """ 102 Itera sobre los elementos SDO_ELEM_INFO de la MDSYS.SDO_GEOMETRY 103 Returns: 104 tuple de integers 105 """ 106 if not self.__SDO_ELEM_INFO: 107 return 108 109 prev_elem_info = None 110 len_elem_info = 3 111 for id_elem in range(0, len(self.__SDO_ELEM_INFO), len_elem_info): 112 elem_info = self.__SDO_ELEM_INFO[id_elem: id_elem + len_elem_info] 113 114 if prev_elem_info: 115 yield (int(prev_elem_info[1]), int(prev_elem_info[2]), 116 (int(prev_elem_info[0] - 1), int(elem_info[0] - 1))) 117 118 prev_elem_info = elem_info 119 120 yield int(prev_elem_info[1]), int(prev_elem_info[2]), (int(prev_elem_info[0] - 1), None) 121 122 def _parse_oracle_params(self): 123 """ 124 Descompone geometría a partir especificación Oracle Spatial para MDSYS.SDO_GEOMETRY e inicializa atributos 125 de self 126 """ 127 m_sdo_gtype = re.match("(\d{1})(\d{1})(\d{2})", str(self.__SDO_GTYPE)) 128 self.__SDO_DIM = int(m_sdo_gtype.group(1)) 129 self.__SDO_LRS = int(m_sdo_gtype.group(2)) 130 self.__DD_SDO_GTYPE = int(m_sdo_gtype.group(3)) 131 132 # Un punto puede venir informado en SDO_POINT y no debería venir informado SDO_ORDINATES ni SDO_ELEM_INFO 133 if self.__SDO_POINT: 134 self.__type_geojson = self.__ora_gtypes_geojson_types.get(1) 135 else: 136 if self.__SDO_DIM not in [2, 3]: 137 print("!AVISO! - Geometria no parseable a formato GEOJSON por NO ser de SDO_DIM 2D o 3D") 138 return 139 140 # Solo se interpretarán los SDO_ETYPE simples segun tabla en siguiente link 141 # https://docs.oracle.com/cd/B28359_01/appdev.111/b28400/sdo_objrelschema.htm#BGHDGCCE 142 sdo_etypes_simples = [1, 2, 1003, 2003] 143 144 num_elems = 0 145 for SDO_ETYPE, SDO_INTERPRETATION, IDXS_SDO_ORDS in self._gen_elems_info(): 146 if SDO_ETYPE not in sdo_etypes_simples: 147 print("!AVISO! - Geometria con elementos no parseables a formato GEOJSON " 148 "por no ser de tipo SDO_ETYPE 1,2,1003,2003") 149 continue 150 151 list_idxs = None 152 if self.__DD_SDO_GTYPE in [1, 5]: # POINT o MULTIPOINT 153 # Angulo del Punto Orientado se añade al ultimo punto 154 if SDO_ETYPE == 1 and SDO_INTERPRETATION == 0: 155 self.__idxs_elems_geom[-1] += IDXS_SDO_ORDS 156 else: 157 list_idxs = self.__idxs_elems_geom 158 num_elems += 1 159 160 elif self.__DD_SDO_GTYPE in [2, 6]: # LINE o MULTILINE 161 list_idxs = self.__idxs_elems_geom 162 num_elems += 1 163 164 elif self.__DD_SDO_GTYPE in [3, 7]: # POLYGON o MULTIPOLYGON 165 list_idxs = self.__idxs_elems_geom 166 m_tip_pol = re.match("(\d{1})(\d*)", str(SDO_ETYPE)) # Si primer digito 1=EXTERIOR si 2=INTERIOR 167 TIP_POL = int(m_tip_pol.group(1)) 168 if TIP_POL == 1: 169 num_elems += 1 170 171 IDXS_SDO_ORDS += (TIP_POL,) 172 173 # Guardamos en el ultimo elemento de la terna de indices para cada elemento su SDO_INTERPRETATION 174 IDXS_SDO_ORDS += (SDO_INTERPRETATION,) 175 176 if list_idxs is not None: 177 list_idxs.append(IDXS_SDO_ORDS) 178 179 if num_elems: 180 self.__type_geojson = self.__ora_gtypes_geojson_types.get(self.__DD_SDO_GTYPE) 181 if num_elems == 1 and self.__DD_SDO_GTYPE > 4: 182 self.__type_geojson = re.sub("Multi", "", self.__type_geojson, 0, re.IGNORECASE) 183 184 def _sdo_ordinates_as_coords(self, idx_ini=0, idx_fi=None): 185 """ 186 Itera sobre las coordenadas (SDO_ORDINATES) de la geometria Oracle MDSYS.SDO_GEOMETRY 187 Args: 188 idx_ini: 189 idx_fi: 190 191 Returns: 192 tuple con las coordenadas 193 """ 194 if self.__SDO_POINT: 195 coords_geojson = (self.__SDO_POINT.X, self.__SDO_POINT.Y) 196 if self.__SDO_POINT.Z: 197 coords_geojson += (self.__SDO_POINT.Z,) 198 199 yield coords_geojson 200 201 elif self.__SDO_ORDINATES: 202 if not idx_fi: 203 idx_fi = len(self.__SDO_ORDINATES) 204 205 for el_dim in range(idx_ini, idx_fi, self.__SDO_DIM): 206 yield tuple(self.__SDO_ORDINATES[el_dim:el_dim + self.__SDO_DIM]) 207 208 def _list_sdo_ordinates_as_coords(self, idx_ini=None, idx_fi=None): 209 """ 210 Devuelve lista de coordenadas redondeadas a 9 decimales por defecto 211 Args: 212 idx_ini: 213 idx_fi: 214 215 Returns: 216 [tuple de coords] 217 """ 218 return [tuple(rounded_float(v) for v in c) for c in self._sdo_ordinates_as_coords(idx_ini, idx_fi)] 219 220 def coords_elems_geom(self, grup_holes=True, inverse_coords=False): 221 """ 222 Devuelve las coordenadas agrupadas por listas para cada sub-elemento (poligono, agujero) que compone la geom 223 Args: 224 grup_holes (bool=True): Por defecto agrupa los agujeros en una lista 225 inverse_coords (bool=False): En Oracle viene en formato LONG-LAT y 226 ahora OGC GDAL desde v3.4 el estandard WKT LAT-LONG 227 228 Returns: 229 list 230 """ 231 coords_elems = [] 232 233 if re.match(r'.*Polygon', self.__type_geojson, re.IGNORECASE): 234 pols = None 235 holes = None 236 for elems_geom in self.iter_elems_geom(): 237 crds = elems_geom[1] 238 if inverse_coords: 239 crds = [(c[1], c[0]) for c in crds] 240 if elems_geom[0] == "Polygon": 241 pols = [] 242 holes = None 243 coords_elems.append(pols) 244 pols.append(crds) 245 else: 246 if not grup_holes: 247 holes = pols 248 elif not holes: 249 holes = [] 250 pols.append(holes) 251 holes.append(crds) 252 253 if len(coords_elems) == 1: 254 coords_elems = coords_elems[0] 255 elif re.match(r'Multi.*', self.__type_geojson, re.IGNORECASE): 256 for elems_geom in self.iter_elems_geom(): 257 crds = elems_geom[1] 258 if inverse_coords: 259 crds = [(c[1], c[0]) for c in crds] 260 coords_elems.append(crds) 261 else: 262 for elems_geom in self.iter_elems_geom(): 263 for c in elems_geom[1]: 264 if inverse_coords and isinstance(c, tuple): 265 c = (c[1], c[0]) 266 coords_elems.append(c) 267 268 return coords_elems 269 270 def angle_points(self): 271 """ 272 Si el tipus de geometria es 'Point' retorna llista d'angles en graus (decimal degrees) respectius 273 a la llista de coordenades o si només és un Point directament el float amb el angle en graus 274 275 Returns: 276 list ó float 277 """ 278 if re.match(r'.*Point', self.__type_geojson, re.IGNORECASE): 279 if re.match(r'Multi.*', self.__type_geojson, re.IGNORECASE): 280 angles_pts = [] 281 for elems_geom in self.iter_elems_geom(): 282 angles_pts.append(degrees(elems_geom[2])) 283 return angles_pts 284 else: 285 return degrees(next(self.iter_elems_geom())[2]) 286 287 @property 288 def simple_tip_geom_geojson(self): 289 """ 290 Devuelve el tipo de geometria simple al que pertence SELF 291 Returns: 292 Point, LineString, Polygon 293 """ 294 return re.sub("Multi", "", self.__type_geojson, 0, re.IGNORECASE) 295 296 def iter_elems_geom(self): 297 """ 298 Retorna segun el tipo de geometria tuples identificando cada elemento que conforma la geometria: 299 Tipo Point -> ("Point", [x,y], angle_radians)) 300 Tipo LineString -> ("LineString", [[x1,y1], [x2,y2], ...]]) 301 Tipo Polygon -> ("Polygon", [[x1,y1], [x2,y2],...]]) 302 ("Hole", [[x1,y1], [x2,y2],...]]) 303 Tipo MultiPoint -> Varios tuples de tipo Point 304 Tipo MultiLineString -> Varios tuples de tipo LineString 305 Tipo MultiPolygon -> Varios tuples de tipo Polygon 306 307 Returns: 308 tuples (tip_geom, list_coords) para cada elementos que conforma la geometria 309 """ 310 simple_type = self.simple_tip_geom_geojson 311 if simple_type == "Point" and not self.__idxs_elems_geom: 312 yield simple_type, next(self._sdo_ordinates_as_coords()), 0 313 else: 314 for idxs in self.__idxs_elems_geom: 315 list_coords = self._list_sdo_ordinates_as_coords(idxs[0], idxs[1]) 316 if simple_type == "Point": 317 angle = 0 318 if len(idxs) == 5: # Punto orientado 319 dx_dy = self._list_sdo_ordinates_as_coords(idxs[-2], idxs[-1])[0] 320 if dx_dy[0] != 0: 321 angle = atan2(dx_dy[1], dx_dy[0]) 322 323 yield simple_type, list_coords[0], angle 324 325 elif simple_type == "Polygon": 326 tip_pol = simple_type 327 if idxs[2] == 2: 328 tip_pol = "Hole" 329 330 # Cuando es tipo poligono y solo vienen 2 pares de coordenadas se revisa su SDO_INTERPRETATION 331 a_sdo_interpretation = idxs[-1] 332 if len(list_coords) == 2 and a_sdo_interpretation == 3: 333 # Será un MBR o optimized rectangle conformado por la esq. inf-izq y la sup-der 334 # Siguiendo la especificacion de GEOJSON se ordenan las coordenadas en el sentido 335 # antihorario siendo la primera y la ultima coordenada las mismas 336 list_coords.insert(1, (list_coords[-1][0], list_coords[0][1])) 337 list_coords.append((list_coords[0][0], list_coords[-1][1])) 338 list_coords.append((list_coords[0][0], list_coords[0][1])) 339 340 yield tip_pol, list_coords 341 else: 342 yield simple_type, list_coords 343 344 def __repr__(self): 345 """ 346 Devuelve string que define la geometria cuando se representa como STRING 347 Returns: 348 str 349 """ 350 l_coords = [",".join(map(lambda x: '{0:.9g}'.format(x), c)) 351 for c in self._sdo_ordinates_as_coords(idx_fi=self.__SDO_DIM)] 352 if self.__DD_SDO_GTYPE not in [1, 5] or len(self.__idxs_elems_geom) > 1: 353 l_coords.append("...") 354 l_txt = [self.__ora_dd_gtypes[self.__DD_SDO_GTYPE], 355 "(SRID=", str(self.__SDO_SRID), 356 ",COORDS=", ",".join(l_coords), ")"] 357 358 return "".join(l_txt) 359 360 def __eq__(self, other): 361 """ 362 Funcion igualdad para cuando se compare con el operador '==' con otro objecto 363 364 Args: 365 other: otro objecto de la clase sdo_geom 366 367 Returns: 368 bool 369 """ 370 371 if isinstance(other, self.__class__): 372 return self.__SDO_GTYPE == other.__SDO_GTYPE and \ 373 self.__SDO_SRID == other.__SDO_SRID and \ 374 self.__SDO_POINT == other.__SDO_POINT and \ 375 self.__SDO_ELEM_INFO == other.__SDO_ELEM_INFO and \ 376 self.__SDO_ORDINATES == other.__SDO_ORDINATES 377 else: 378 return False 379 380 def __ne__(self, other): 381 """ 382 Funcion NO igual para cuando se compare con el operador '!=' con otro objecto 383 384 Args: 385 other: otro objecto de la clase sdo_geom 386 387 Returns: 388 bool 389 """ 390 return not self.__eq__(other) 391 392 @property 393 def tip_geom(self): 394 """ 395 Devuelve tipo de geometria: 396 "UNKNOWN_GEOMETRY", 397 "POINT", 398 "LINE", 399 "POLYGON", 400 "COLLECTION", 401 "MULTIPOINT", 402 "MULTILINE", 403 "MULTIPOLYGON", 404 "SOLID", 405 "MULTISOLID" 406 407 Returns: 408 tip_geom 409 """ 410 return self.__ora_dd_gtypes[self.__DD_SDO_GTYPE] 411 412 def as_ora_sdo_geometry(self): 413 """ 414 Devuelve self como un oracle sdo_geometry 415 416 Returns: 417 cx_Oracle type object MDSYS.SDO_GEOMETRY 418 """ 419 if not self.__ora_sdo_geometry: 420 ora_sdo_geom_type = self.__con_db.gettype("MDSYS.SDO_GEOMETRY") 421 ora_sdo_geom = ora_sdo_geom_type.newobject() 422 ora_sdo_geom.SDO_GTYPE = self.__SDO_GTYPE 423 ora_sdo_geom.SDO_SRID = self.__SDO_SRID 424 425 if self.__SDO_POINT: 426 ora_sdo_geom.SDO_POINT = self.__SDO_POINT 427 else: 428 elementInfoTypeObj = self.__con_db.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") 429 ordinateTypeObj = self.__con_db.gettype("MDSYS.SDO_ORDINATE_ARRAY") 430 ora_sdo_geom.SDO_ELEM_INFO = elementInfoTypeObj.newobject() 431 ora_sdo_geom.SDO_ELEM_INFO.extend(self.__SDO_ELEM_INFO) 432 ora_sdo_geom.SDO_ORDINATES = ordinateTypeObj.newobject() 433 ora_sdo_geom.SDO_ORDINATES.extend(self.__SDO_ORDINATES) 434 435 self.__ora_sdo_geometry = ora_sdo_geom 436 437 return self.__ora_sdo_geometry 438 439 def as_shapely(self): 440 """ 441 Retorna self como una geom del modulo shapely 442 443 Returns: 444 clase geometria del modulo shapely 445 """ 446 if not self.__shapely_obj and self.__type_geojson: 447 shp_geom = shape(self.__geo_interface__) 448 if self.__SDO_SRID != 4326: 449 shp_geom = shapely_utils.transform_shapely_geom(shp_geom, 4326, self.__SDO_SRID) 450 self.__shapely_obj = shp_geom 451 452 return self.__shapely_obj 453 454 def as_wkt(self): 455 """ 456 Retorna geom en formato WKT 457 458 Returns: 459 str 460 """ 461 ogr_geom = self.as_ogr_geom() 462 if ogr_geom: 463 return ogr_geom.ExportToWkt() 464 465 def as_wkb(self): 466 """ 467 Retorna geom en formato WKB 468 469 Returns: 470 b(str): binary string 471 """ 472 ogr_geom = self.as_ogr_geom() 473 if ogr_geom: 474 return ogr_geom.ExportToWkb() 475 476 def as_ogr_geom(self): 477 """ 478 Retorna la geometria como instancia de geometria de la libreria ogr 479 480 Returns: 481 osgeo.ogr.Geometry 482 """ 483 if not self.__ogr_geom and self.__type_geojson: 484 self.__ogr_geom = ogr.CreateGeometryFromJson(self.as_geojson()) 485 486 return self.__ogr_geom 487 488 def as_gml(self): 489 """ 490 Retorna geom en format GML 491 492 Returns: 493 str en formato GML 494 """ 495 ogr_g = self.as_ogr_geom() 496 if ogr_g: 497 return ogr_g.ExportToGML() 498 499 def as_kml(self): 500 """ 501 Retorna geom en format KML 502 503 Returns: 504 str en formato KML 505 """ 506 ogr_g = self.as_ogr_geom() 507 if ogr_g: 508 return ogr_g.ExportToKML() 509 510 def as_geojson(self): 511 """ 512 Retorna geom en format GeoJson 513 514 Returns: 515 str en formato GeoJson 516 """ 517 return json.dumps(self.__geo_interface__) 518 519 @property 520 def __geo_interface__(self): 521 """ 522 Python protocol for geospatial data (GeoJson always in EPSG4326 lat-long) 523 524 Returns: 525 dict que representa geom en formato GeoJson 526 """ 527 geom_geojson = { 528 "type": self.__type_geojson, 529 "coordinates": self.transform_to_srid(4326).coords_elems_geom(grup_holes=False) 530 } 531 532 return geom_geojson 533 534 def transform_to_srid(self, srid): 535 """ 536 Retorna la geometría transformada al SRID de Oracle dado (suele ser codigo numérico EPSG) 537 538 Args: 539 srid: codigo epsg 540 541 Returns: 542 sdo_geom transformada al SRID 543 """ 544 from apb_cx_oracle_spatial import gestor_oracle 545 546 if int(srid) == int(self.__SDO_SRID): 547 return self 548 549 geom_trans = self.__cache_transformed_geoms.get(srid) 550 if not geom_trans: 551 nom_col = "GEOM" 552 553 try: 554 sql = "select sdo_cs.transform(:1, :2) as {nom_col} from dual".format(nom_col=nom_col) 555 row_ret = gestor_oracle.execute_fetch_sql( 556 self.__con_db, 557 sql, 558 self.as_ora_sdo_geometry(), 559 srid, 560 output_handler=get_output_handler(self.__con_db)) 561 562 geom_trans = getattr(row_ret, nom_col) 563 self._set_transformed_geom(srid, geom_trans) 564 # Se guarda en la nueva sdo_geom creada SELF 565 geom_trans._set_transformed_geom(self.__SDO_SRID, self) 566 except: 567 raise Exception( 568 "!ERROR en metodo sdo_geom.transform_to_srid() para geometria {}!".format( 569 self)) 570 571 return geom_trans 572 573 def _set_transformed_geom(self, srid, a_sdo_geom): 574 """ 575 Cachea sobre self las transformaciones a distintos SRID 576 577 Args: 578 srid: 579 a_sdo_geom: 580 """ 581 if self != a_sdo_geom: 582 self.__cache_transformed_geoms[srid] = a_sdo_geom 583 584 def convert_to(self, a_format, srid=None): 585 """ 586 Retorna la geometría en los formatos WKT, KML o GML (disponibles en Oracle) o JSON/GEOJSON 587 (son el mismo) y en el sistema de coordenadas especificado 588 589 Args: 590 a_format: 591 srid: 592 593 Returns: 594 object o str con el formato especificado 595 """ 596 formats = {"JSON": "ExportToJson", 597 "GEOJSON": "ExportToJson", 598 "WKT": "ExportToWkt", 599 "WKB": "ExportToWkb", 600 "GML": "ExportToGml", 601 "XML": "ExportToGml", 602 "KML": "ExportToKml"} 603 604 a_format = a_format.upper() 605 geom_formatted = self.__cache_formatted_geoms.get((a_format, srid)) 606 if not geom_formatted: 607 a_ogr_geom = self.as_ogr_geom() 608 if srid and srid != self.__SDO_SRID: 609 a_ogr_geom = apb_extra_osgeo_utils.transform_ogr_geom(a_ogr_geom, self.__SDO_SRID, srid) 610 611 if a_format in formats: 612 geom_formatted = getattr(a_ogr_geom, 613 formats[a_format])() 614 else: 615 raise Exception("!ERROR! - Formato '" + a_format + 616 "' no disponible para sdo_geom.convert_to()") 617 618 if geom_formatted: 619 self.__cache_formatted_geoms[(a_format, srid)] = geom_formatted 620 621 return geom_formatted 622 623 624# Handler de Input y Output para los cursores de cx_Oracle para devolver las geometrías como instancias de la SDO_GEOM 625def get_build_sdo_geom(con_db=None, func_format_geom=None): 626 """ 627 Retorna la funcion decorada que devolverá el objeto SDO_GEOM y si viene 628 la llamada a partir de una ROW de una tabla o vista entonces también 629 llegará informada la conexión cx_oracle (CON_DB) y el tipo de class_tip_geom que 630 que está asociado a la columna de la geometría (TIP_GEOM) 631 632 Args: 633 con_db: 634 func_format_geom: 635 636 Returns: 637 sdo_geom ó formato retorno de la funcion func_format_geom 638 """ 639 640 def build_sdo_geom(cx_oracle_obj): 641 if cx_oracle_obj: 642 try: 643 a_geom = sdo_geom(cx_oracle_obj, con_db) 644 if func_format_geom: 645 a_geom = getattr(a_geom, func_format_geom)() 646 647 return a_geom 648 except Exception: 649 return cx_oracle_obj 650 651 return build_sdo_geom 652 653 654def get_output_handler(con_db, func_format_geom=None): 655 """ 656 Retorna el handler para tratar geometrías oracle Sdo_geometry y convertirlas a SDO_GEOM. 657 Se podrá indicar como dicccionario de nombres columna: class_tip_geom el tipo en concreto que tendrá asociado 658 cada nueva SDO_GEOM creada 659 660 Args: 661 con_db: 662 func_format_geom: nombre funcion conversion sobre SDO_GEOM a otro formato de geometria 663 664 Returns: 665 output_type_handler configurado dependiendo de valor 666 """ 667 668 def output_type_handler(cursor, name_field, defaultType, length, precision, scale): 669 if defaultType in [cx_Oracle.Object, cx_Oracle.OBJECT]: 670 return cursor.var(defaultType, arraysize=cursor.arraysize, 671 outconverter=get_build_sdo_geom(con_db, func_format_geom), 672 typename="MDSYS.SDO_GEOMETRY") 673 674 elif defaultType in [cx_Oracle.STRING, cx_Oracle.FIXED_CHAR] and sys.version_info[0] < 3: 675 return cursor.var(defaultType, arraysize=cursor.arraysize, 676 outconverter=lambda val_campo: val_campo.decode(con_db.encoding).encode('UTF-8')) 677 678 return output_type_handler 679 680 681def sdo_geom_in_converter(a_sdo_geom): 682 """ 683 Devuelve cx_Oracle.Object SDO_GEOMETRY a partir de SDO_GEOM 684 685 Args: 686 a_sdo_geom: instancia clase sdo_geom 687 688 Returns: 689 cx_Oracle.SDO_GEOMETRY 690 """ 691 try: 692 return a_sdo_geom.as_ora_sdo_geometry() 693 except Exception: 694 print("!ERROR en apb_cx_oracle_spatial.get_sdo_geom_in_converter() al convertir clase sdo_geom a " 695 "cx_Oracle.Object!") 696 return None 697 698 699def get_sdo_input_handler(): 700 """ 701 Devuelve funcion para tratar los SDO_GEOM en transacciones sql como MDSYS.SDO_GEOMETRY 702 703 Returns: 704 input_type_handler configurado para convertir las sdo_geom a MDSYS.SDO_GEOMETRY 705 """ 706 707 def sdo_input_handler(cursor, value, num_elems): 708 if isinstance(value, sdo_geom): 709 return cursor.var(cx_Oracle.OBJECT, arraysize=num_elems, 710 inconverter=sdo_geom_in_converter, 711 typename="MDSYS.SDO_GEOMETRY") 712 713 return sdo_input_handler
23class sdo_geom(object): 24 """ 25 Clase que instanciará objectos MDSYS.SDO_GEOMETRY devueltos por cx_Oracle añadiendo funcionalidad para convertir a 26 distintos formatos geométricos. Se representará como una geometria geojson a partir de interfaz _geo_interface_ 27 """ 28 __slots__ = ["__SDO_GTYPE", 29 "__SDO_SRID", 30 "__SDO_POINT", 31 "__SDO_ELEM_INFO", 32 "__SDO_ORDINATES", 33 "__SDO_DIM", 34 "__SDO_LRS", 35 "__DD_SDO_GTYPE", 36 "__type_geojson", 37 "__idxs_elems_geom", 38 "__con_db", 39 "__ora_sdo_geometry", 40 "__shapely_obj", 41 "__ogr_geom", 42 "__cache_transformed_geoms", 43 "__cache_formatted_geoms"] 44 45 __ora_dd_gtypes = {0: "UNKNOWN_GEOMETRY", 46 1: "POINT", 47 2: "LINE", # "CURVE", 48 3: "POLYGON", # "SURFACE", 49 4: "COLLECTION", 50 5: "MULTIPOINT", 51 6: "MULTILINE", # "MULTICURVE", 52 7: "MULTIPOLYGON", # "MULTISURFACE", 53 8: "SOLID", 54 9: "MULTISOLID"} 55 56 __ora_gtypes_geojson_types = {1: "Point", 57 5: "MultiPoint", 58 2: "LineString", 59 6: "MultiLineString", 60 3: "Polygon", 61 7: "MultiPolygon", 62 4: "GeometryCollection"} 63 64 def __init__(self, cx_ora_sdo_geom, con_ora_db): 65 """ 66 Genera instancia para geometria Oracle MDSYS.SDO_GEOMETRY 67 68 Args: 69 cx_ora_sdo_geom: valor MDSYS.SDO_GEOMETRY a partir de cx_Oracle 70 con_ora_db: conexion Oracle cx_Oracle.Connection 71 """ 72 self.__SDO_GTYPE = int(cx_ora_sdo_geom.SDO_GTYPE) 73 self.__SDO_SRID = int(cx_ora_sdo_geom.SDO_SRID) 74 75 sdo_point_orig = cx_ora_sdo_geom.SDO_POINT 76 a_sdo_point = None 77 l_elem_info = None 78 l_ordinates = None 79 if sdo_point_orig: 80 a_sdo_point = sdo_point_orig.copy() 81 else: 82 l_elem_info = cx_ora_sdo_geom.SDO_ELEM_INFO.aslist() 83 l_ordinates = cx_ora_sdo_geom.SDO_ORDINATES.aslist() 84 85 self.__SDO_POINT = a_sdo_point 86 self.__SDO_ELEM_INFO = l_elem_info 87 self.__SDO_ORDINATES = l_ordinates 88 89 self.__con_db = con_ora_db 90 91 self.__idxs_elems_geom = [] 92 93 self._parse_oracle_params() 94 95 self.__ora_sdo_geometry = None 96 self.__shapely_obj = None 97 self.__ogr_geom = None 98 self.__cache_transformed_geoms = {} 99 self.__cache_formatted_geoms = {} 100 101 def _gen_elems_info(self): 102 """ 103 Itera sobre los elementos SDO_ELEM_INFO de la MDSYS.SDO_GEOMETRY 104 Returns: 105 tuple de integers 106 """ 107 if not self.__SDO_ELEM_INFO: 108 return 109 110 prev_elem_info = None 111 len_elem_info = 3 112 for id_elem in range(0, len(self.__SDO_ELEM_INFO), len_elem_info): 113 elem_info = self.__SDO_ELEM_INFO[id_elem: id_elem + len_elem_info] 114 115 if prev_elem_info: 116 yield (int(prev_elem_info[1]), int(prev_elem_info[2]), 117 (int(prev_elem_info[0] - 1), int(elem_info[0] - 1))) 118 119 prev_elem_info = elem_info 120 121 yield int(prev_elem_info[1]), int(prev_elem_info[2]), (int(prev_elem_info[0] - 1), None) 122 123 def _parse_oracle_params(self): 124 """ 125 Descompone geometría a partir especificación Oracle Spatial para MDSYS.SDO_GEOMETRY e inicializa atributos 126 de self 127 """ 128 m_sdo_gtype = re.match("(\d{1})(\d{1})(\d{2})", str(self.__SDO_GTYPE)) 129 self.__SDO_DIM = int(m_sdo_gtype.group(1)) 130 self.__SDO_LRS = int(m_sdo_gtype.group(2)) 131 self.__DD_SDO_GTYPE = int(m_sdo_gtype.group(3)) 132 133 # Un punto puede venir informado en SDO_POINT y no debería venir informado SDO_ORDINATES ni SDO_ELEM_INFO 134 if self.__SDO_POINT: 135 self.__type_geojson = self.__ora_gtypes_geojson_types.get(1) 136 else: 137 if self.__SDO_DIM not in [2, 3]: 138 print("!AVISO! - Geometria no parseable a formato GEOJSON por NO ser de SDO_DIM 2D o 3D") 139 return 140 141 # Solo se interpretarán los SDO_ETYPE simples segun tabla en siguiente link 142 # https://docs.oracle.com/cd/B28359_01/appdev.111/b28400/sdo_objrelschema.htm#BGHDGCCE 143 sdo_etypes_simples = [1, 2, 1003, 2003] 144 145 num_elems = 0 146 for SDO_ETYPE, SDO_INTERPRETATION, IDXS_SDO_ORDS in self._gen_elems_info(): 147 if SDO_ETYPE not in sdo_etypes_simples: 148 print("!AVISO! - Geometria con elementos no parseables a formato GEOJSON " 149 "por no ser de tipo SDO_ETYPE 1,2,1003,2003") 150 continue 151 152 list_idxs = None 153 if self.__DD_SDO_GTYPE in [1, 5]: # POINT o MULTIPOINT 154 # Angulo del Punto Orientado se añade al ultimo punto 155 if SDO_ETYPE == 1 and SDO_INTERPRETATION == 0: 156 self.__idxs_elems_geom[-1] += IDXS_SDO_ORDS 157 else: 158 list_idxs = self.__idxs_elems_geom 159 num_elems += 1 160 161 elif self.__DD_SDO_GTYPE in [2, 6]: # LINE o MULTILINE 162 list_idxs = self.__idxs_elems_geom 163 num_elems += 1 164 165 elif self.__DD_SDO_GTYPE in [3, 7]: # POLYGON o MULTIPOLYGON 166 list_idxs = self.__idxs_elems_geom 167 m_tip_pol = re.match("(\d{1})(\d*)", str(SDO_ETYPE)) # Si primer digito 1=EXTERIOR si 2=INTERIOR 168 TIP_POL = int(m_tip_pol.group(1)) 169 if TIP_POL == 1: 170 num_elems += 1 171 172 IDXS_SDO_ORDS += (TIP_POL,) 173 174 # Guardamos en el ultimo elemento de la terna de indices para cada elemento su SDO_INTERPRETATION 175 IDXS_SDO_ORDS += (SDO_INTERPRETATION,) 176 177 if list_idxs is not None: 178 list_idxs.append(IDXS_SDO_ORDS) 179 180 if num_elems: 181 self.__type_geojson = self.__ora_gtypes_geojson_types.get(self.__DD_SDO_GTYPE) 182 if num_elems == 1 and self.__DD_SDO_GTYPE > 4: 183 self.__type_geojson = re.sub("Multi", "", self.__type_geojson, 0, re.IGNORECASE) 184 185 def _sdo_ordinates_as_coords(self, idx_ini=0, idx_fi=None): 186 """ 187 Itera sobre las coordenadas (SDO_ORDINATES) de la geometria Oracle MDSYS.SDO_GEOMETRY 188 Args: 189 idx_ini: 190 idx_fi: 191 192 Returns: 193 tuple con las coordenadas 194 """ 195 if self.__SDO_POINT: 196 coords_geojson = (self.__SDO_POINT.X, self.__SDO_POINT.Y) 197 if self.__SDO_POINT.Z: 198 coords_geojson += (self.__SDO_POINT.Z,) 199 200 yield coords_geojson 201 202 elif self.__SDO_ORDINATES: 203 if not idx_fi: 204 idx_fi = len(self.__SDO_ORDINATES) 205 206 for el_dim in range(idx_ini, idx_fi, self.__SDO_DIM): 207 yield tuple(self.__SDO_ORDINATES[el_dim:el_dim + self.__SDO_DIM]) 208 209 def _list_sdo_ordinates_as_coords(self, idx_ini=None, idx_fi=None): 210 """ 211 Devuelve lista de coordenadas redondeadas a 9 decimales por defecto 212 Args: 213 idx_ini: 214 idx_fi: 215 216 Returns: 217 [tuple de coords] 218 """ 219 return [tuple(rounded_float(v) for v in c) for c in self._sdo_ordinates_as_coords(idx_ini, idx_fi)] 220 221 def coords_elems_geom(self, grup_holes=True, inverse_coords=False): 222 """ 223 Devuelve las coordenadas agrupadas por listas para cada sub-elemento (poligono, agujero) que compone la geom 224 Args: 225 grup_holes (bool=True): Por defecto agrupa los agujeros en una lista 226 inverse_coords (bool=False): En Oracle viene en formato LONG-LAT y 227 ahora OGC GDAL desde v3.4 el estandard WKT LAT-LONG 228 229 Returns: 230 list 231 """ 232 coords_elems = [] 233 234 if re.match(r'.*Polygon', self.__type_geojson, re.IGNORECASE): 235 pols = None 236 holes = None 237 for elems_geom in self.iter_elems_geom(): 238 crds = elems_geom[1] 239 if inverse_coords: 240 crds = [(c[1], c[0]) for c in crds] 241 if elems_geom[0] == "Polygon": 242 pols = [] 243 holes = None 244 coords_elems.append(pols) 245 pols.append(crds) 246 else: 247 if not grup_holes: 248 holes = pols 249 elif not holes: 250 holes = [] 251 pols.append(holes) 252 holes.append(crds) 253 254 if len(coords_elems) == 1: 255 coords_elems = coords_elems[0] 256 elif re.match(r'Multi.*', self.__type_geojson, re.IGNORECASE): 257 for elems_geom in self.iter_elems_geom(): 258 crds = elems_geom[1] 259 if inverse_coords: 260 crds = [(c[1], c[0]) for c in crds] 261 coords_elems.append(crds) 262 else: 263 for elems_geom in self.iter_elems_geom(): 264 for c in elems_geom[1]: 265 if inverse_coords and isinstance(c, tuple): 266 c = (c[1], c[0]) 267 coords_elems.append(c) 268 269 return coords_elems 270 271 def angle_points(self): 272 """ 273 Si el tipus de geometria es 'Point' retorna llista d'angles en graus (decimal degrees) respectius 274 a la llista de coordenades o si només és un Point directament el float amb el angle en graus 275 276 Returns: 277 list ó float 278 """ 279 if re.match(r'.*Point', self.__type_geojson, re.IGNORECASE): 280 if re.match(r'Multi.*', self.__type_geojson, re.IGNORECASE): 281 angles_pts = [] 282 for elems_geom in self.iter_elems_geom(): 283 angles_pts.append(degrees(elems_geom[2])) 284 return angles_pts 285 else: 286 return degrees(next(self.iter_elems_geom())[2]) 287 288 @property 289 def simple_tip_geom_geojson(self): 290 """ 291 Devuelve el tipo de geometria simple al que pertence SELF 292 Returns: 293 Point, LineString, Polygon 294 """ 295 return re.sub("Multi", "", self.__type_geojson, 0, re.IGNORECASE) 296 297 def iter_elems_geom(self): 298 """ 299 Retorna segun el tipo de geometria tuples identificando cada elemento que conforma la geometria: 300 Tipo Point -> ("Point", [x,y], angle_radians)) 301 Tipo LineString -> ("LineString", [[x1,y1], [x2,y2], ...]]) 302 Tipo Polygon -> ("Polygon", [[x1,y1], [x2,y2],...]]) 303 ("Hole", [[x1,y1], [x2,y2],...]]) 304 Tipo MultiPoint -> Varios tuples de tipo Point 305 Tipo MultiLineString -> Varios tuples de tipo LineString 306 Tipo MultiPolygon -> Varios tuples de tipo Polygon 307 308 Returns: 309 tuples (tip_geom, list_coords) para cada elementos que conforma la geometria 310 """ 311 simple_type = self.simple_tip_geom_geojson 312 if simple_type == "Point" and not self.__idxs_elems_geom: 313 yield simple_type, next(self._sdo_ordinates_as_coords()), 0 314 else: 315 for idxs in self.__idxs_elems_geom: 316 list_coords = self._list_sdo_ordinates_as_coords(idxs[0], idxs[1]) 317 if simple_type == "Point": 318 angle = 0 319 if len(idxs) == 5: # Punto orientado 320 dx_dy = self._list_sdo_ordinates_as_coords(idxs[-2], idxs[-1])[0] 321 if dx_dy[0] != 0: 322 angle = atan2(dx_dy[1], dx_dy[0]) 323 324 yield simple_type, list_coords[0], angle 325 326 elif simple_type == "Polygon": 327 tip_pol = simple_type 328 if idxs[2] == 2: 329 tip_pol = "Hole" 330 331 # Cuando es tipo poligono y solo vienen 2 pares de coordenadas se revisa su SDO_INTERPRETATION 332 a_sdo_interpretation = idxs[-1] 333 if len(list_coords) == 2 and a_sdo_interpretation == 3: 334 # Será un MBR o optimized rectangle conformado por la esq. inf-izq y la sup-der 335 # Siguiendo la especificacion de GEOJSON se ordenan las coordenadas en el sentido 336 # antihorario siendo la primera y la ultima coordenada las mismas 337 list_coords.insert(1, (list_coords[-1][0], list_coords[0][1])) 338 list_coords.append((list_coords[0][0], list_coords[-1][1])) 339 list_coords.append((list_coords[0][0], list_coords[0][1])) 340 341 yield tip_pol, list_coords 342 else: 343 yield simple_type, list_coords 344 345 def __repr__(self): 346 """ 347 Devuelve string que define la geometria cuando se representa como STRING 348 Returns: 349 str 350 """ 351 l_coords = [",".join(map(lambda x: '{0:.9g}'.format(x), c)) 352 for c in self._sdo_ordinates_as_coords(idx_fi=self.__SDO_DIM)] 353 if self.__DD_SDO_GTYPE not in [1, 5] or len(self.__idxs_elems_geom) > 1: 354 l_coords.append("...") 355 l_txt = [self.__ora_dd_gtypes[self.__DD_SDO_GTYPE], 356 "(SRID=", str(self.__SDO_SRID), 357 ",COORDS=", ",".join(l_coords), ")"] 358 359 return "".join(l_txt) 360 361 def __eq__(self, other): 362 """ 363 Funcion igualdad para cuando se compare con el operador '==' con otro objecto 364 365 Args: 366 other: otro objecto de la clase sdo_geom 367 368 Returns: 369 bool 370 """ 371 372 if isinstance(other, self.__class__): 373 return self.__SDO_GTYPE == other.__SDO_GTYPE and \ 374 self.__SDO_SRID == other.__SDO_SRID and \ 375 self.__SDO_POINT == other.__SDO_POINT and \ 376 self.__SDO_ELEM_INFO == other.__SDO_ELEM_INFO and \ 377 self.__SDO_ORDINATES == other.__SDO_ORDINATES 378 else: 379 return False 380 381 def __ne__(self, other): 382 """ 383 Funcion NO igual para cuando se compare con el operador '!=' con otro objecto 384 385 Args: 386 other: otro objecto de la clase sdo_geom 387 388 Returns: 389 bool 390 """ 391 return not self.__eq__(other) 392 393 @property 394 def tip_geom(self): 395 """ 396 Devuelve tipo de geometria: 397 "UNKNOWN_GEOMETRY", 398 "POINT", 399 "LINE", 400 "POLYGON", 401 "COLLECTION", 402 "MULTIPOINT", 403 "MULTILINE", 404 "MULTIPOLYGON", 405 "SOLID", 406 "MULTISOLID" 407 408 Returns: 409 tip_geom 410 """ 411 return self.__ora_dd_gtypes[self.__DD_SDO_GTYPE] 412 413 def as_ora_sdo_geometry(self): 414 """ 415 Devuelve self como un oracle sdo_geometry 416 417 Returns: 418 cx_Oracle type object MDSYS.SDO_GEOMETRY 419 """ 420 if not self.__ora_sdo_geometry: 421 ora_sdo_geom_type = self.__con_db.gettype("MDSYS.SDO_GEOMETRY") 422 ora_sdo_geom = ora_sdo_geom_type.newobject() 423 ora_sdo_geom.SDO_GTYPE = self.__SDO_GTYPE 424 ora_sdo_geom.SDO_SRID = self.__SDO_SRID 425 426 if self.__SDO_POINT: 427 ora_sdo_geom.SDO_POINT = self.__SDO_POINT 428 else: 429 elementInfoTypeObj = self.__con_db.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") 430 ordinateTypeObj = self.__con_db.gettype("MDSYS.SDO_ORDINATE_ARRAY") 431 ora_sdo_geom.SDO_ELEM_INFO = elementInfoTypeObj.newobject() 432 ora_sdo_geom.SDO_ELEM_INFO.extend(self.__SDO_ELEM_INFO) 433 ora_sdo_geom.SDO_ORDINATES = ordinateTypeObj.newobject() 434 ora_sdo_geom.SDO_ORDINATES.extend(self.__SDO_ORDINATES) 435 436 self.__ora_sdo_geometry = ora_sdo_geom 437 438 return self.__ora_sdo_geometry 439 440 def as_shapely(self): 441 """ 442 Retorna self como una geom del modulo shapely 443 444 Returns: 445 clase geometria del modulo shapely 446 """ 447 if not self.__shapely_obj and self.__type_geojson: 448 shp_geom = shape(self.__geo_interface__) 449 if self.__SDO_SRID != 4326: 450 shp_geom = shapely_utils.transform_shapely_geom(shp_geom, 4326, self.__SDO_SRID) 451 self.__shapely_obj = shp_geom 452 453 return self.__shapely_obj 454 455 def as_wkt(self): 456 """ 457 Retorna geom en formato WKT 458 459 Returns: 460 str 461 """ 462 ogr_geom = self.as_ogr_geom() 463 if ogr_geom: 464 return ogr_geom.ExportToWkt() 465 466 def as_wkb(self): 467 """ 468 Retorna geom en formato WKB 469 470 Returns: 471 b(str): binary string 472 """ 473 ogr_geom = self.as_ogr_geom() 474 if ogr_geom: 475 return ogr_geom.ExportToWkb() 476 477 def as_ogr_geom(self): 478 """ 479 Retorna la geometria como instancia de geometria de la libreria ogr 480 481 Returns: 482 osgeo.ogr.Geometry 483 """ 484 if not self.__ogr_geom and self.__type_geojson: 485 self.__ogr_geom = ogr.CreateGeometryFromJson(self.as_geojson()) 486 487 return self.__ogr_geom 488 489 def as_gml(self): 490 """ 491 Retorna geom en format GML 492 493 Returns: 494 str en formato GML 495 """ 496 ogr_g = self.as_ogr_geom() 497 if ogr_g: 498 return ogr_g.ExportToGML() 499 500 def as_kml(self): 501 """ 502 Retorna geom en format KML 503 504 Returns: 505 str en formato KML 506 """ 507 ogr_g = self.as_ogr_geom() 508 if ogr_g: 509 return ogr_g.ExportToKML() 510 511 def as_geojson(self): 512 """ 513 Retorna geom en format GeoJson 514 515 Returns: 516 str en formato GeoJson 517 """ 518 return json.dumps(self.__geo_interface__) 519 520 @property 521 def __geo_interface__(self): 522 """ 523 Python protocol for geospatial data (GeoJson always in EPSG4326 lat-long) 524 525 Returns: 526 dict que representa geom en formato GeoJson 527 """ 528 geom_geojson = { 529 "type": self.__type_geojson, 530 "coordinates": self.transform_to_srid(4326).coords_elems_geom(grup_holes=False) 531 } 532 533 return geom_geojson 534 535 def transform_to_srid(self, srid): 536 """ 537 Retorna la geometría transformada al SRID de Oracle dado (suele ser codigo numérico EPSG) 538 539 Args: 540 srid: codigo epsg 541 542 Returns: 543 sdo_geom transformada al SRID 544 """ 545 from apb_cx_oracle_spatial import gestor_oracle 546 547 if int(srid) == int(self.__SDO_SRID): 548 return self 549 550 geom_trans = self.__cache_transformed_geoms.get(srid) 551 if not geom_trans: 552 nom_col = "GEOM" 553 554 try: 555 sql = "select sdo_cs.transform(:1, :2) as {nom_col} from dual".format(nom_col=nom_col) 556 row_ret = gestor_oracle.execute_fetch_sql( 557 self.__con_db, 558 sql, 559 self.as_ora_sdo_geometry(), 560 srid, 561 output_handler=get_output_handler(self.__con_db)) 562 563 geom_trans = getattr(row_ret, nom_col) 564 self._set_transformed_geom(srid, geom_trans) 565 # Se guarda en la nueva sdo_geom creada SELF 566 geom_trans._set_transformed_geom(self.__SDO_SRID, self) 567 except: 568 raise Exception( 569 "!ERROR en metodo sdo_geom.transform_to_srid() para geometria {}!".format( 570 self)) 571 572 return geom_trans 573 574 def _set_transformed_geom(self, srid, a_sdo_geom): 575 """ 576 Cachea sobre self las transformaciones a distintos SRID 577 578 Args: 579 srid: 580 a_sdo_geom: 581 """ 582 if self != a_sdo_geom: 583 self.__cache_transformed_geoms[srid] = a_sdo_geom 584 585 def convert_to(self, a_format, srid=None): 586 """ 587 Retorna la geometría en los formatos WKT, KML o GML (disponibles en Oracle) o JSON/GEOJSON 588 (son el mismo) y en el sistema de coordenadas especificado 589 590 Args: 591 a_format: 592 srid: 593 594 Returns: 595 object o str con el formato especificado 596 """ 597 formats = {"JSON": "ExportToJson", 598 "GEOJSON": "ExportToJson", 599 "WKT": "ExportToWkt", 600 "WKB": "ExportToWkb", 601 "GML": "ExportToGml", 602 "XML": "ExportToGml", 603 "KML": "ExportToKml"} 604 605 a_format = a_format.upper() 606 geom_formatted = self.__cache_formatted_geoms.get((a_format, srid)) 607 if not geom_formatted: 608 a_ogr_geom = self.as_ogr_geom() 609 if srid and srid != self.__SDO_SRID: 610 a_ogr_geom = apb_extra_osgeo_utils.transform_ogr_geom(a_ogr_geom, self.__SDO_SRID, srid) 611 612 if a_format in formats: 613 geom_formatted = getattr(a_ogr_geom, 614 formats[a_format])() 615 else: 616 raise Exception("!ERROR! - Formato '" + a_format + 617 "' no disponible para sdo_geom.convert_to()") 618 619 if geom_formatted: 620 self.__cache_formatted_geoms[(a_format, srid)] = geom_formatted 621 622 return geom_formatted
Clase que instanciará objectos MDSYS.SDO_GEOMETRY devueltos por cx_Oracle añadiendo funcionalidad para convertir a distintos formatos geométricos. Se representará como una geometria geojson a partir de interfaz _geo_interface_
64 def __init__(self, cx_ora_sdo_geom, con_ora_db): 65 """ 66 Genera instancia para geometria Oracle MDSYS.SDO_GEOMETRY 67 68 Args: 69 cx_ora_sdo_geom: valor MDSYS.SDO_GEOMETRY a partir de cx_Oracle 70 con_ora_db: conexion Oracle cx_Oracle.Connection 71 """ 72 self.__SDO_GTYPE = int(cx_ora_sdo_geom.SDO_GTYPE) 73 self.__SDO_SRID = int(cx_ora_sdo_geom.SDO_SRID) 74 75 sdo_point_orig = cx_ora_sdo_geom.SDO_POINT 76 a_sdo_point = None 77 l_elem_info = None 78 l_ordinates = None 79 if sdo_point_orig: 80 a_sdo_point = sdo_point_orig.copy() 81 else: 82 l_elem_info = cx_ora_sdo_geom.SDO_ELEM_INFO.aslist() 83 l_ordinates = cx_ora_sdo_geom.SDO_ORDINATES.aslist() 84 85 self.__SDO_POINT = a_sdo_point 86 self.__SDO_ELEM_INFO = l_elem_info 87 self.__SDO_ORDINATES = l_ordinates 88 89 self.__con_db = con_ora_db 90 91 self.__idxs_elems_geom = [] 92 93 self._parse_oracle_params() 94 95 self.__ora_sdo_geometry = None 96 self.__shapely_obj = None 97 self.__ogr_geom = None 98 self.__cache_transformed_geoms = {} 99 self.__cache_formatted_geoms = {}
Genera instancia para geometria Oracle MDSYS.SDO_GEOMETRY
Arguments:
- cx_ora_sdo_geom: valor MDSYS.SDO_GEOMETRY a partir de cx_Oracle
- con_ora_db: conexion Oracle cx_Oracle.Connection
221 def coords_elems_geom(self, grup_holes=True, inverse_coords=False): 222 """ 223 Devuelve las coordenadas agrupadas por listas para cada sub-elemento (poligono, agujero) que compone la geom 224 Args: 225 grup_holes (bool=True): Por defecto agrupa los agujeros en una lista 226 inverse_coords (bool=False): En Oracle viene en formato LONG-LAT y 227 ahora OGC GDAL desde v3.4 el estandard WKT LAT-LONG 228 229 Returns: 230 list 231 """ 232 coords_elems = [] 233 234 if re.match(r'.*Polygon', self.__type_geojson, re.IGNORECASE): 235 pols = None 236 holes = None 237 for elems_geom in self.iter_elems_geom(): 238 crds = elems_geom[1] 239 if inverse_coords: 240 crds = [(c[1], c[0]) for c in crds] 241 if elems_geom[0] == "Polygon": 242 pols = [] 243 holes = None 244 coords_elems.append(pols) 245 pols.append(crds) 246 else: 247 if not grup_holes: 248 holes = pols 249 elif not holes: 250 holes = [] 251 pols.append(holes) 252 holes.append(crds) 253 254 if len(coords_elems) == 1: 255 coords_elems = coords_elems[0] 256 elif re.match(r'Multi.*', self.__type_geojson, re.IGNORECASE): 257 for elems_geom in self.iter_elems_geom(): 258 crds = elems_geom[1] 259 if inverse_coords: 260 crds = [(c[1], c[0]) for c in crds] 261 coords_elems.append(crds) 262 else: 263 for elems_geom in self.iter_elems_geom(): 264 for c in elems_geom[1]: 265 if inverse_coords and isinstance(c, tuple): 266 c = (c[1], c[0]) 267 coords_elems.append(c) 268 269 return coords_elems
Devuelve las coordenadas agrupadas por listas para cada sub-elemento (poligono, agujero) que compone la geom
Arguments:
- grup_holes (bool=True): Por defecto agrupa los agujeros en una lista
- inverse_coords (bool=False): En Oracle viene en formato LONG-LAT y ahora OGC GDAL desde v3.4 el estandard WKT LAT-LONG
Returns:
list
271 def angle_points(self): 272 """ 273 Si el tipus de geometria es 'Point' retorna llista d'angles en graus (decimal degrees) respectius 274 a la llista de coordenades o si només és un Point directament el float amb el angle en graus 275 276 Returns: 277 list ó float 278 """ 279 if re.match(r'.*Point', self.__type_geojson, re.IGNORECASE): 280 if re.match(r'Multi.*', self.__type_geojson, re.IGNORECASE): 281 angles_pts = [] 282 for elems_geom in self.iter_elems_geom(): 283 angles_pts.append(degrees(elems_geom[2])) 284 return angles_pts 285 else: 286 return degrees(next(self.iter_elems_geom())[2])
Si el tipus de geometria es 'Point' retorna llista d'angles en graus (decimal degrees) respectius a la llista de coordenades o si només és un Point directament el float amb el angle en graus
Returns:
list ó float
288 @property 289 def simple_tip_geom_geojson(self): 290 """ 291 Devuelve el tipo de geometria simple al que pertence SELF 292 Returns: 293 Point, LineString, Polygon 294 """ 295 return re.sub("Multi", "", self.__type_geojson, 0, re.IGNORECASE)
Devuelve el tipo de geometria simple al que pertence SELF
Returns:
Point, LineString, Polygon
297 def iter_elems_geom(self): 298 """ 299 Retorna segun el tipo de geometria tuples identificando cada elemento que conforma la geometria: 300 Tipo Point -> ("Point", [x,y], angle_radians)) 301 Tipo LineString -> ("LineString", [[x1,y1], [x2,y2], ...]]) 302 Tipo Polygon -> ("Polygon", [[x1,y1], [x2,y2],...]]) 303 ("Hole", [[x1,y1], [x2,y2],...]]) 304 Tipo MultiPoint -> Varios tuples de tipo Point 305 Tipo MultiLineString -> Varios tuples de tipo LineString 306 Tipo MultiPolygon -> Varios tuples de tipo Polygon 307 308 Returns: 309 tuples (tip_geom, list_coords) para cada elementos que conforma la geometria 310 """ 311 simple_type = self.simple_tip_geom_geojson 312 if simple_type == "Point" and not self.__idxs_elems_geom: 313 yield simple_type, next(self._sdo_ordinates_as_coords()), 0 314 else: 315 for idxs in self.__idxs_elems_geom: 316 list_coords = self._list_sdo_ordinates_as_coords(idxs[0], idxs[1]) 317 if simple_type == "Point": 318 angle = 0 319 if len(idxs) == 5: # Punto orientado 320 dx_dy = self._list_sdo_ordinates_as_coords(idxs[-2], idxs[-1])[0] 321 if dx_dy[0] != 0: 322 angle = atan2(dx_dy[1], dx_dy[0]) 323 324 yield simple_type, list_coords[0], angle 325 326 elif simple_type == "Polygon": 327 tip_pol = simple_type 328 if idxs[2] == 2: 329 tip_pol = "Hole" 330 331 # Cuando es tipo poligono y solo vienen 2 pares de coordenadas se revisa su SDO_INTERPRETATION 332 a_sdo_interpretation = idxs[-1] 333 if len(list_coords) == 2 and a_sdo_interpretation == 3: 334 # Será un MBR o optimized rectangle conformado por la esq. inf-izq y la sup-der 335 # Siguiendo la especificacion de GEOJSON se ordenan las coordenadas en el sentido 336 # antihorario siendo la primera y la ultima coordenada las mismas 337 list_coords.insert(1, (list_coords[-1][0], list_coords[0][1])) 338 list_coords.append((list_coords[0][0], list_coords[-1][1])) 339 list_coords.append((list_coords[0][0], list_coords[0][1])) 340 341 yield tip_pol, list_coords 342 else: 343 yield simple_type, list_coords
Retorna segun el tipo de geometria tuples identificando cada elemento que conforma la geometria:
Tipo Point -> ("Point", [x,y], angle_radians)) Tipo LineString -> ("LineString", [[x1,y1], [x2,y2], ...]]) Tipo Polygon -> ("Polygon", [[x1,y1], [x2,y2],...]]) ("Hole", [[x1,y1], [x2,y2],...]]) Tipo MultiPoint -> Varios tuples de tipo Point Tipo MultiLineString -> Varios tuples de tipo LineString Tipo MultiPolygon -> Varios tuples de tipo Polygon
Returns:
tuples (tip_geom, list_coords) para cada elementos que conforma la geometria
393 @property 394 def tip_geom(self): 395 """ 396 Devuelve tipo de geometria: 397 "UNKNOWN_GEOMETRY", 398 "POINT", 399 "LINE", 400 "POLYGON", 401 "COLLECTION", 402 "MULTIPOINT", 403 "MULTILINE", 404 "MULTIPOLYGON", 405 "SOLID", 406 "MULTISOLID" 407 408 Returns: 409 tip_geom 410 """ 411 return self.__ora_dd_gtypes[self.__DD_SDO_GTYPE]
Devuelve tipo de geometria:
"UNKNOWN_GEOMETRY", "POINT", "LINE", "POLYGON", "COLLECTION", "MULTIPOINT", "MULTILINE", "MULTIPOLYGON", "SOLID", "MULTISOLID"
Returns:
tip_geom
413 def as_ora_sdo_geometry(self): 414 """ 415 Devuelve self como un oracle sdo_geometry 416 417 Returns: 418 cx_Oracle type object MDSYS.SDO_GEOMETRY 419 """ 420 if not self.__ora_sdo_geometry: 421 ora_sdo_geom_type = self.__con_db.gettype("MDSYS.SDO_GEOMETRY") 422 ora_sdo_geom = ora_sdo_geom_type.newobject() 423 ora_sdo_geom.SDO_GTYPE = self.__SDO_GTYPE 424 ora_sdo_geom.SDO_SRID = self.__SDO_SRID 425 426 if self.__SDO_POINT: 427 ora_sdo_geom.SDO_POINT = self.__SDO_POINT 428 else: 429 elementInfoTypeObj = self.__con_db.gettype("MDSYS.SDO_ELEM_INFO_ARRAY") 430 ordinateTypeObj = self.__con_db.gettype("MDSYS.SDO_ORDINATE_ARRAY") 431 ora_sdo_geom.SDO_ELEM_INFO = elementInfoTypeObj.newobject() 432 ora_sdo_geom.SDO_ELEM_INFO.extend(self.__SDO_ELEM_INFO) 433 ora_sdo_geom.SDO_ORDINATES = ordinateTypeObj.newobject() 434 ora_sdo_geom.SDO_ORDINATES.extend(self.__SDO_ORDINATES) 435 436 self.__ora_sdo_geometry = ora_sdo_geom 437 438 return self.__ora_sdo_geometry
Devuelve self como un oracle sdo_geometry
Returns:
cx_Oracle type object MDSYS.SDO_GEOMETRY
440 def as_shapely(self): 441 """ 442 Retorna self como una geom del modulo shapely 443 444 Returns: 445 clase geometria del modulo shapely 446 """ 447 if not self.__shapely_obj and self.__type_geojson: 448 shp_geom = shape(self.__geo_interface__) 449 if self.__SDO_SRID != 4326: 450 shp_geom = shapely_utils.transform_shapely_geom(shp_geom, 4326, self.__SDO_SRID) 451 self.__shapely_obj = shp_geom 452 453 return self.__shapely_obj
Retorna self como una geom del modulo shapely
Returns:
clase geometria del modulo shapely
455 def as_wkt(self): 456 """ 457 Retorna geom en formato WKT 458 459 Returns: 460 str 461 """ 462 ogr_geom = self.as_ogr_geom() 463 if ogr_geom: 464 return ogr_geom.ExportToWkt()
Retorna geom en formato WKT
Returns:
str
466 def as_wkb(self): 467 """ 468 Retorna geom en formato WKB 469 470 Returns: 471 b(str): binary string 472 """ 473 ogr_geom = self.as_ogr_geom() 474 if ogr_geom: 475 return ogr_geom.ExportToWkb()
Retorna geom en formato WKB
Returns:
b(str): binary string
477 def as_ogr_geom(self): 478 """ 479 Retorna la geometria como instancia de geometria de la libreria ogr 480 481 Returns: 482 osgeo.ogr.Geometry 483 """ 484 if not self.__ogr_geom and self.__type_geojson: 485 self.__ogr_geom = ogr.CreateGeometryFromJson(self.as_geojson()) 486 487 return self.__ogr_geom
Retorna la geometria como instancia de geometria de la libreria ogr
Returns:
osgeo.ogr.Geometry
489 def as_gml(self): 490 """ 491 Retorna geom en format GML 492 493 Returns: 494 str en formato GML 495 """ 496 ogr_g = self.as_ogr_geom() 497 if ogr_g: 498 return ogr_g.ExportToGML()
Retorna geom en format GML
Returns:
str en formato GML
500 def as_kml(self): 501 """ 502 Retorna geom en format KML 503 504 Returns: 505 str en formato KML 506 """ 507 ogr_g = self.as_ogr_geom() 508 if ogr_g: 509 return ogr_g.ExportToKML()
Retorna geom en format KML
Returns:
str en formato KML
511 def as_geojson(self): 512 """ 513 Retorna geom en format GeoJson 514 515 Returns: 516 str en formato GeoJson 517 """ 518 return json.dumps(self.__geo_interface__)
Retorna geom en format GeoJson
Returns:
str en formato GeoJson
535 def transform_to_srid(self, srid): 536 """ 537 Retorna la geometría transformada al SRID de Oracle dado (suele ser codigo numérico EPSG) 538 539 Args: 540 srid: codigo epsg 541 542 Returns: 543 sdo_geom transformada al SRID 544 """ 545 from apb_cx_oracle_spatial import gestor_oracle 546 547 if int(srid) == int(self.__SDO_SRID): 548 return self 549 550 geom_trans = self.__cache_transformed_geoms.get(srid) 551 if not geom_trans: 552 nom_col = "GEOM" 553 554 try: 555 sql = "select sdo_cs.transform(:1, :2) as {nom_col} from dual".format(nom_col=nom_col) 556 row_ret = gestor_oracle.execute_fetch_sql( 557 self.__con_db, 558 sql, 559 self.as_ora_sdo_geometry(), 560 srid, 561 output_handler=get_output_handler(self.__con_db)) 562 563 geom_trans = getattr(row_ret, nom_col) 564 self._set_transformed_geom(srid, geom_trans) 565 # Se guarda en la nueva sdo_geom creada SELF 566 geom_trans._set_transformed_geom(self.__SDO_SRID, self) 567 except: 568 raise Exception( 569 "!ERROR en metodo sdo_geom.transform_to_srid() para geometria {}!".format( 570 self)) 571 572 return geom_trans
Retorna la geometría transformada al SRID de Oracle dado (suele ser codigo numérico EPSG)
Arguments:
- srid: codigo epsg
Returns:
sdo_geom transformada al SRID
585 def convert_to(self, a_format, srid=None): 586 """ 587 Retorna la geometría en los formatos WKT, KML o GML (disponibles en Oracle) o JSON/GEOJSON 588 (son el mismo) y en el sistema de coordenadas especificado 589 590 Args: 591 a_format: 592 srid: 593 594 Returns: 595 object o str con el formato especificado 596 """ 597 formats = {"JSON": "ExportToJson", 598 "GEOJSON": "ExportToJson", 599 "WKT": "ExportToWkt", 600 "WKB": "ExportToWkb", 601 "GML": "ExportToGml", 602 "XML": "ExportToGml", 603 "KML": "ExportToKml"} 604 605 a_format = a_format.upper() 606 geom_formatted = self.__cache_formatted_geoms.get((a_format, srid)) 607 if not geom_formatted: 608 a_ogr_geom = self.as_ogr_geom() 609 if srid and srid != self.__SDO_SRID: 610 a_ogr_geom = apb_extra_osgeo_utils.transform_ogr_geom(a_ogr_geom, self.__SDO_SRID, srid) 611 612 if a_format in formats: 613 geom_formatted = getattr(a_ogr_geom, 614 formats[a_format])() 615 else: 616 raise Exception("!ERROR! - Formato '" + a_format + 617 "' no disponible para sdo_geom.convert_to()") 618 619 if geom_formatted: 620 self.__cache_formatted_geoms[(a_format, srid)] = geom_formatted 621 622 return geom_formatted
Retorna la geometría en los formatos WKT, KML o GML (disponibles en Oracle) o JSON/GEOJSON (son el mismo) y en el sistema de coordenadas especificado
Arguments:
- a_format:
- srid:
Returns:
object o str con el formato especificado
626def get_build_sdo_geom(con_db=None, func_format_geom=None): 627 """ 628 Retorna la funcion decorada que devolverá el objeto SDO_GEOM y si viene 629 la llamada a partir de una ROW de una tabla o vista entonces también 630 llegará informada la conexión cx_oracle (CON_DB) y el tipo de class_tip_geom que 631 que está asociado a la columna de la geometría (TIP_GEOM) 632 633 Args: 634 con_db: 635 func_format_geom: 636 637 Returns: 638 sdo_geom ó formato retorno de la funcion func_format_geom 639 """ 640 641 def build_sdo_geom(cx_oracle_obj): 642 if cx_oracle_obj: 643 try: 644 a_geom = sdo_geom(cx_oracle_obj, con_db) 645 if func_format_geom: 646 a_geom = getattr(a_geom, func_format_geom)() 647 648 return a_geom 649 except Exception: 650 return cx_oracle_obj 651 652 return build_sdo_geom
Retorna la funcion decorada que devolverá el objeto SDO_GEOM y si viene la llamada a partir de una ROW de una tabla o vista entonces también llegará informada la conexión cx_oracle (CON_DB) y el tipo de class_tip_geom que que está asociado a la columna de la geometría (TIP_GEOM)
Arguments:
- con_db:
- func_format_geom:
Returns:
sdo_geom ó formato retorno de la funcion func_format_geom
655def get_output_handler(con_db, func_format_geom=None): 656 """ 657 Retorna el handler para tratar geometrías oracle Sdo_geometry y convertirlas a SDO_GEOM. 658 Se podrá indicar como dicccionario de nombres columna: class_tip_geom el tipo en concreto que tendrá asociado 659 cada nueva SDO_GEOM creada 660 661 Args: 662 con_db: 663 func_format_geom: nombre funcion conversion sobre SDO_GEOM a otro formato de geometria 664 665 Returns: 666 output_type_handler configurado dependiendo de valor 667 """ 668 669 def output_type_handler(cursor, name_field, defaultType, length, precision, scale): 670 if defaultType in [cx_Oracle.Object, cx_Oracle.OBJECT]: 671 return cursor.var(defaultType, arraysize=cursor.arraysize, 672 outconverter=get_build_sdo_geom(con_db, func_format_geom), 673 typename="MDSYS.SDO_GEOMETRY") 674 675 elif defaultType in [cx_Oracle.STRING, cx_Oracle.FIXED_CHAR] and sys.version_info[0] < 3: 676 return cursor.var(defaultType, arraysize=cursor.arraysize, 677 outconverter=lambda val_campo: val_campo.decode(con_db.encoding).encode('UTF-8')) 678 679 return output_type_handler
Retorna el handler para tratar geometrías oracle Sdo_geometry y convertirlas a SDO_GEOM. Se podrá indicar como dicccionario de nombres columna: class_tip_geom el tipo en concreto que tendrá asociado cada nueva SDO_GEOM creada
Arguments:
- con_db:
- func_format_geom: nombre funcion conversion sobre SDO_GEOM a otro formato de geometria
Returns:
output_type_handler configurado dependiendo de valor
682def sdo_geom_in_converter(a_sdo_geom): 683 """ 684 Devuelve cx_Oracle.Object SDO_GEOMETRY a partir de SDO_GEOM 685 686 Args: 687 a_sdo_geom: instancia clase sdo_geom 688 689 Returns: 690 cx_Oracle.SDO_GEOMETRY 691 """ 692 try: 693 return a_sdo_geom.as_ora_sdo_geometry() 694 except Exception: 695 print("!ERROR en apb_cx_oracle_spatial.get_sdo_geom_in_converter() al convertir clase sdo_geom a " 696 "cx_Oracle.Object!") 697 return None
Devuelve cx_Oracle.Object SDO_GEOMETRY a partir de SDO_GEOM
Arguments:
- a_sdo_geom: instancia clase sdo_geom
Returns:
cx_Oracle.SDO_GEOMETRY
700def get_sdo_input_handler(): 701 """ 702 Devuelve funcion para tratar los SDO_GEOM en transacciones sql como MDSYS.SDO_GEOMETRY 703 704 Returns: 705 input_type_handler configurado para convertir las sdo_geom a MDSYS.SDO_GEOMETRY 706 """ 707 708 def sdo_input_handler(cursor, value, num_elems): 709 if isinstance(value, sdo_geom): 710 return cursor.var(cx_Oracle.OBJECT, arraysize=num_elems, 711 inconverter=sdo_geom_in_converter, 712 typename="MDSYS.SDO_GEOMETRY") 713 714 return sdo_input_handler
Devuelve funcion para tratar los SDO_GEOM en transacciones sql como MDSYS.SDO_GEOMETRY
Returns:
input_type_handler configurado para convertir las sdo_geom a MDSYS.SDO_GEOMETRY