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
class sdo_geom:
 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_

sdo_geom(cx_ora_sdo_geom, con_ora_db)
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
def coords_elems_geom(self, grup_holes=True, inverse_coords=False):
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

def angle_points(self):
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

simple_tip_geom_geojson
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

def iter_elems_geom(self):
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

tip_geom
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

def as_ora_sdo_geometry(self):
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

def as_shapely(self):
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

def as_wkt(self):
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

def as_wkb(self):
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

def as_ogr_geom(self):
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

def as_gml(self):
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

def as_kml(self):
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

def as_geojson(self):
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

def transform_to_srid(self, srid):
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

def convert_to(self, a_format, srid=None):
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

def get_build_sdo_geom(con_db=None, func_format_geom=None):
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

def get_output_handler(con_db, func_format_geom=None):
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

def sdo_geom_in_converter(a_sdo_geom):
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

def get_sdo_input_handler():
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