1"""
2Proxies for libgeos, GEOS-specific exceptions, and utilities
3"""
4
5import atexit
6from ctypes import (
7    CDLL, cdll, pointer, string_at, DEFAULT_MODE, c_void_p, c_size_t, c_char_p)
8from ctypes.util import find_library
9import glob
10import logging
11import os
12import re
13import sys
14import threading
15from functools import partial
16
17from .ctypes_declarations import prototype, EXCEPTION_HANDLER_FUNCTYPE
18from .errors import InvalidGeometryError, WKBReadingError, WKTReadingError, TopologicalError, PredicateError
19
20
21# Add message handler to this module's logger
22LOG = logging.getLogger(__name__)
23
24# Find and load the GEOS and C libraries
25# If this ever gets any longer, we'll break it into separate modules
26
27def load_dll(libname, fallbacks=None, mode=DEFAULT_MODE):
28    lib = find_library(libname)
29    dll = None
30    if lib is not None:
31        try:
32            LOG.debug("Trying `CDLL(%s)`", lib)
33            dll = CDLL(lib, mode=mode)
34        except OSError:
35            LOG.debug("Failed `CDLL(%s)`", lib)
36            pass
37
38    if not dll and fallbacks is not None:
39        for name in fallbacks:
40            try:
41                LOG.debug("Trying `CDLL(%s)`", name)
42                dll = CDLL(name, mode=mode)
43            except OSError:
44                # move on to the next fallback
45                LOG.debug("Failed `CDLL(%s)`", name)
46                pass
47
48    if dll:
49        LOG.debug("Library path: %r", lib or name)
50        LOG.debug("DLL: %r", dll)
51        return dll
52    else:
53        # No shared library was loaded. Raise OSError.
54        raise OSError(
55            "Could not find lib {} or load any of its variants {}.".format(
56                libname, fallbacks or []))
57
58_lgeos = None
59def exists_conda_env():
60    """Does this module exist in a conda environment?"""
61    return os.path.exists(os.path.join(sys.prefix, 'conda-meta'))
62
63
64if sys.platform.startswith('linux'):
65    # Test to see if we have a wheel repaired by auditwheel which contains its
66    # own libgeos_c. Note: auditwheel 3.1 changed the location of libs.
67    geos_whl_so = glob.glob(
68        os.path.abspath(os.path.join(os.path.dirname(__file__), ".libs/libgeos*.so*"))
69    ) or glob.glob(
70        os.path.abspath(
71            os.path.join(
72                os.path.dirname(__file__), "..", "Shapely.libs", "libgeos*.so*"
73            )
74        )
75    )
76
77    if len(geos_whl_so) > 0:
78        # We have observed problems with CDLL of libgeos_c not automatically
79        # loading the sibling c++ library since the change made by auditwheel
80        # 3.1, so we explicitly load them both.
81        geos_whl_so = sorted(geos_whl_so)
82        CDLL(geos_whl_so[0])
83        _lgeos = CDLL(geos_whl_so[-1])
84        LOG.debug("Found GEOS DLL: %r, using it.", _lgeos)
85
86    elif hasattr(sys, 'frozen'):
87        geos_pyinstaller_so = glob.glob(os.path.join(sys.prefix, 'libgeos_c-*.so.*'))
88        if len(geos_pyinstaller_so) >= 1:
89            _lgeos = CDLL(geos_pyinstaller_so[0])
90            LOG.debug("Found GEOS DLL: %r, using it.", _lgeos)
91    elif exists_conda_env():
92        # conda package.
93        _lgeos = CDLL(os.path.join(sys.prefix, 'lib', 'libgeos_c.so'))
94    else:
95        alt_paths = [
96            'libgeos_c.so.1',
97            'libgeos_c.so',
98        ]
99        _lgeos = load_dll('geos_c', fallbacks=alt_paths)
100
101    # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen
102    # manpage says, "If filename is NULL, then the returned handle is for the
103    # main program". This way we can let the linker do the work to figure out
104    # which libc Python is actually using.
105    free = CDLL(None).free
106    free.argtypes = [c_void_p]
107    free.restype = None
108
109elif sys.platform == 'darwin':
110    # Test to see if we have a delocated wheel with a GEOS dylib.
111    geos_whl_dylib = os.path.abspath(os.path.join(os.path.dirname(
112        __file__), '.dylibs/libgeos_c.1.dylib'))
113
114    if os.path.exists(geos_whl_dylib):
115        handle = CDLL(None)
116        if hasattr(handle, "initGEOS_r"):
117            LOG.debug("GEOS already loaded")
118            _lgeos = handle
119        else:
120            _lgeos = CDLL(geos_whl_dylib)
121            LOG.debug("Found GEOS DLL: %r, using it.", _lgeos)
122
123    elif exists_conda_env():
124        # conda package.
125        _lgeos = CDLL(os.path.join(sys.prefix, 'lib', 'libgeos_c.dylib'))
126    else:
127        if hasattr(sys, 'frozen'):
128            try:
129                # .app file from py2app
130                alt_paths = [os.path.join(
131                    os.environ['RESOURCEPATH'], '..', 'Frameworks',
132                    'libgeos_c.dylib')]
133            except KeyError:
134                alt_paths = [
135                    # binary from pyinstaller
136                    os.path.join(sys.executable, 'libgeos_c.dylib'),
137                    # .app from cx_Freeze
138                    os.path.join(os.path.dirname(sys.executable), 'libgeos_c.1.dylib')]
139                if hasattr(sys, '_MEIPASS'):
140                    alt_paths.append(
141                        os.path.join(sys._MEIPASS, 'libgeos_c.1.dylib'))
142        else:
143            alt_paths = [
144                # The Framework build from Kyng Chaos
145                "/Library/Frameworks/GEOS.framework/Versions/Current/GEOS",
146                # macports
147                '/opt/local/lib/libgeos_c.dylib',
148                # homebrew Intel
149                '/usr/local/lib/libgeos_c.dylib',
150                # homebrew Apple Silicon
151                '/opt/homebrew/lib/libgeos_c.dylib',
152            ]
153        _lgeos = load_dll('geos_c', fallbacks=alt_paths)
154
155    free = CDLL(None).free
156    free.argtypes = [c_void_p]
157    free.restype = None
158
159elif sys.platform == 'win32':
160    _conda_dll_path = os.path.join(sys.prefix, 'Library', 'bin', 'geos_c.dll')
161    if exists_conda_env() and os.path.exists(_conda_dll_path):
162        # conda package.
163        _lgeos = CDLL(_conda_dll_path)
164    else:
165        try:
166            egg_dlls = os.path.abspath(
167                os.path.join(os.path.dirname(__file__), 'DLLs'))
168            if hasattr(sys, '_MEIPASS'):
169                wininst_dlls = sys._MEIPASS
170            elif hasattr(sys, "frozen"):
171                wininst_dlls = os.path.normpath(
172                    os.path.abspath(sys.executable + '../../DLLS'))
173            else:
174                wininst_dlls = os.path.abspath(os.__file__ + "../../../DLLs")
175            original_path = os.environ['PATH']
176            os.environ['PATH'] = "%s;%s;%s" % \
177                (egg_dlls, wininst_dlls, original_path)
178            _lgeos = load_dll("geos_c.dll")
179        except (ImportError, WindowsError, OSError):
180            raise
181
182        def free(m):
183            try:
184                cdll.msvcrt.free(m)
185            except WindowsError:
186                # XXX: See http://trac.gispython.org/projects/PCL/ticket/149
187                pass
188
189elif sys.platform == 'sunos5':
190    _lgeos = load_dll('geos_c', fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
191    free.restype = None
192    free.argtypes = [c_void_p]
193    free.restype = None
194
195else:  # other *nix systems
196    _lgeos = load_dll('geos_c', fallbacks=['libgeos_c.so.1', 'libgeos_c.so'])
197    free = CDLL(None).free
198    free.argtypes = [c_void_p]
199    free.restype = None
200
201
202def _geos_version():
203    GEOSversion = _lgeos.GEOSversion
204    GEOSversion.restype = c_char_p
205    GEOSversion.argtypes = []
206    geos_version_string = GEOSversion().decode('ascii')
207    res = re.findall(r'(\d+)\.(\d+)\.(\d+)', geos_version_string)
208    assert len(res) == 2, res
209    geos_version = tuple(int(x) for x in res[0])
210    capi_version = tuple(int(x) for x in res[1])
211    return geos_version_string, geos_version, capi_version
212
213geos_version_string, geos_version, geos_capi_version = _geos_version()
214
215
216# Record a baseline so that we know what additional functions are declared
217# in ctypes_declarations.
218start_set = set(_lgeos.__dict__)
219
220# Apply prototypes for the libgeos_c functions
221prototype(_lgeos, geos_version)
222
223# Automatically detect all function declarations, and declare their
224# re-entrant counterpart.
225end_set = set(_lgeos.__dict__)
226new_func_names = end_set - start_set
227
228for func_name in new_func_names:
229    new_func_name = "%s_r" % func_name
230    if hasattr(_lgeos, new_func_name):
231        new_func = getattr(_lgeos, new_func_name)
232        old_func = getattr(_lgeos, func_name)
233        new_func.restype = old_func.restype
234        if old_func.argtypes is None:
235            # Handle functions that didn't take an argument before,
236            # finishGEOS.
237            new_func.argtypes = [c_void_p]
238        else:
239            new_func.argtypes = [c_void_p] + list(old_func.argtypes)
240        if old_func.errcheck is not None:
241            new_func.errcheck = old_func.errcheck
242
243# Handle special case.
244_lgeos.initGEOS_r.restype = c_void_p
245_lgeos.initGEOS_r.argtypes = \
246    [EXCEPTION_HANDLER_FUNCTYPE, EXCEPTION_HANDLER_FUNCTYPE]
247_lgeos.finishGEOS_r.argtypes = [c_void_p]
248
249
250def make_logging_callback(func):
251    """Error or notice handler callback producr
252
253    Wraps a logger method, func, as a GEOS callback.
254    """
255    def callback(fmt, *fmt_args):
256        fmt = fmt.decode('ascii')
257        conversions = re.findall(r'%.', fmt)
258        args = [
259            string_at(arg).decode('ascii')
260            for spec, arg in zip(conversions, fmt_args)
261            if spec == '%s' and arg is not None]
262
263        func(fmt, *args)
264
265    return callback
266
267error_handler = make_logging_callback(LOG.error)
268notice_handler = make_logging_callback(LOG.info)
269
270error_h = EXCEPTION_HANDLER_FUNCTYPE(error_handler)
271notice_h = EXCEPTION_HANDLER_FUNCTYPE(notice_handler)
272
273
274class WKTReader:
275
276    _lgeos = None
277    _reader = None
278
279    def __init__(self, lgeos):
280        """Create WKT Reader"""
281        self._lgeos = lgeos
282        self._reader = self._lgeos.GEOSWKTReader_create()
283
284    def __del__(self):
285        """Destroy WKT Reader"""
286        if self._lgeos is not None:
287            self._lgeos.GEOSWKTReader_destroy(self._reader)
288            self._reader = None
289            self._lgeos = None
290
291    def read(self, text):
292        """Returns geometry from WKT"""
293        if not isinstance(text, str):
294            raise TypeError("Only str is accepted.")
295        text = text.encode()
296        c_string = c_char_p(text)
297        geom = self._lgeos.GEOSWKTReader_read(self._reader, c_string)
298        if not geom:
299            raise WKTReadingError(
300                "Could not create geometry because of errors "
301                "while reading input.")
302        # avoid circular import dependency
303        from shapely.geometry.base import geom_factory
304        return geom_factory(geom)
305
306
307class WKTWriter:
308
309    _lgeos = None
310    _writer = None
311
312    # Establish default output settings
313    defaults = {
314        'trim': True,
315        'output_dimension': 3,
316    }
317
318    # GEOS' defaults for methods without "get"
319    _trim = False
320    _rounding_precision = -1
321    _old_3d = False
322
323    @property
324    def trim(self):
325        """Trimming of unnecessary decimals (default: True)"""
326        return getattr(self, '_trim')
327
328    @trim.setter
329    def trim(self, value):
330        self._trim = bool(value)
331        self._lgeos.GEOSWKTWriter_setTrim(self._writer, self._trim)
332
333    @property
334    def rounding_precision(self):
335        """Rounding precision when writing the WKT.
336        A precision of -1 (default) disables it."""
337        return getattr(self, '_rounding_precision')
338
339    @rounding_precision.setter
340    def rounding_precision(self, value):
341        self._rounding_precision = int(value)
342        self._lgeos.GEOSWKTWriter_setRoundingPrecision(
343            self._writer, self._rounding_precision)
344
345    @property
346    def output_dimension(self):
347        """Output dimension, either 2 or 3 (default)"""
348        return self._lgeos.GEOSWKTWriter_getOutputDimension(
349            self._writer)
350
351    @output_dimension.setter
352    def output_dimension(self, value):
353        self._lgeos.GEOSWKTWriter_setOutputDimension(
354            self._writer, int(value))
355
356    @property
357    def old_3d(self):
358        """Show older style for 3D WKT, without 'Z' (default: False)"""
359        return getattr(self, '_old_3d')
360
361    @old_3d.setter
362    def old_3d(self, value):
363        self._old_3d = bool(value)
364        self._lgeos.GEOSWKTWriter_setOld3D(self._writer, self._old_3d)
365
366    def __init__(self, lgeos, **settings):
367        """Create WKT Writer
368
369        Note: older formatting before GEOS 3.3.0 can be achieved by setting
370        the properties:
371            trim = False
372            output_dimension = 2
373        """
374        self._lgeos = lgeos
375        self._writer = self._lgeos.GEOSWKTWriter_create()
376
377        applied_settings = self.defaults.copy()
378        applied_settings.update(settings)
379        for name in applied_settings:
380            setattr(self, name, applied_settings[name])
381
382    def __setattr__(self, name, value):
383        """Limit setting attributes"""
384        if hasattr(self, name):
385            object.__setattr__(self, name, value)
386        else:
387            raise AttributeError('%r object has no attribute %r' %
388                                 (self.__class__.__name__, name))
389
390    def __del__(self):
391        """Destroy WKT Writer"""
392        if self._lgeos is not None:
393            self._lgeos.GEOSWKTWriter_destroy(self._writer)
394            self._writer = None
395            self._lgeos = None
396
397    def write(self, geom):
398        """Returns WKT string for geometry"""
399        if geom is None or geom._geom is None:
400            raise InvalidGeometryError("Null geometry supports no operations")
401        result = self._lgeos.GEOSWKTWriter_write(self._writer, geom._geom)
402        text = string_at(result)
403        lgeos.GEOSFree(result)
404        return text.decode('ascii')
405
406
407class WKBReader:
408
409    _lgeos = None
410    _reader = None
411
412    def __init__(self, lgeos):
413        """Create WKB Reader"""
414        self._lgeos = lgeos
415        self._reader = self._lgeos.GEOSWKBReader_create()
416
417    def __del__(self):
418        """Destroy WKB Reader"""
419        if self._lgeos is not None:
420            self._lgeos.GEOSWKBReader_destroy(self._reader)
421            self._reader = None
422            self._lgeos = None
423
424    def read(self, data):
425        """Returns geometry from WKB"""
426        geom = self._lgeos.GEOSWKBReader_read(
427            self._reader, c_char_p(data), c_size_t(len(data)))
428        if not geom:
429            raise WKBReadingError(
430                "Could not create geometry because of errors "
431                "while reading input.")
432        # avoid circular import dependency
433        from shapely import geometry
434        return geometry.base.geom_factory(geom)
435
436    def read_hex(self, data):
437        """Returns geometry from WKB hex"""
438        data = data.encode('ascii')
439        geom = self._lgeos.GEOSWKBReader_readHEX(
440            self._reader, c_char_p(data), c_size_t(len(data)))
441        if not geom:
442            raise WKBReadingError(
443                "Could not create geometry because of errors "
444                "while reading input.")
445        # avoid circular import dependency
446        from shapely import geometry
447        return geometry.base.geom_factory(geom)
448
449
450class WKBWriter:
451
452    _lgeos = None
453    _writer = None
454
455    # EndianType enum in ByteOrderValues.h
456    _ENDIAN_BIG = 0
457    _ENDIAN_LITTLE = 1
458
459    # Establish default output setting
460    defaults = {'output_dimension': 3}
461
462    @property
463    def output_dimension(self):
464        """Output dimension, either 2 or 3 (default)"""
465        return self._lgeos.GEOSWKBWriter_getOutputDimension(self._writer)
466
467    @output_dimension.setter
468    def output_dimension(self, value):
469        self._lgeos.GEOSWKBWriter_setOutputDimension(
470            self._writer, int(value))
471
472    @property
473    def big_endian(self):
474        """Byte order is big endian, True (default) or False"""
475        return (self._lgeos.GEOSWKBWriter_getByteOrder(self._writer) ==
476                self._ENDIAN_BIG)
477
478    @big_endian.setter
479    def big_endian(self, value):
480        self._lgeos.GEOSWKBWriter_setByteOrder(
481            self._writer, self._ENDIAN_BIG if value else self._ENDIAN_LITTLE)
482
483    @property
484    def include_srid(self):
485        """Include SRID, True or False (default)"""
486        return bool(self._lgeos.GEOSWKBWriter_getIncludeSRID(self._writer))
487
488    @include_srid.setter
489    def include_srid(self, value):
490        self._lgeos.GEOSWKBWriter_setIncludeSRID(self._writer, bool(value))
491
492    def __init__(self, lgeos, **settings):
493        """Create WKB Writer"""
494        self._lgeos = lgeos
495        self._writer = self._lgeos.GEOSWKBWriter_create()
496
497        applied_settings = self.defaults.copy()
498        applied_settings.update(settings)
499        for name in applied_settings:
500            setattr(self, name, applied_settings[name])
501
502    def __setattr__(self, name, value):
503        """Limit setting attributes"""
504        if hasattr(self, name):
505            object.__setattr__(self, name, value)
506        else:
507            raise AttributeError('%r object has no attribute %r' %
508                                 (self.__class__.__name__, name))
509
510    def __del__(self):
511        """Destroy WKB Writer"""
512        if self._lgeos is not None:
513            self._lgeos.GEOSWKBWriter_destroy(self._writer)
514            self._writer = None
515            self._lgeos = None
516
517    def write(self, geom):
518        """Returns WKB byte string for geometry"""
519        if geom is None or geom._geom is None:
520            raise InvalidGeometryError("Null geometry supports no operations")
521        size = c_size_t()
522        result = self._lgeos.GEOSWKBWriter_write(
523            self._writer, geom._geom, pointer(size))
524        data = string_at(result, size.value)
525        lgeos.GEOSFree(result)
526        return data
527
528    def write_hex(self, geom):
529        """Returns WKB hex string for geometry"""
530        if geom is None or geom._geom is None:
531            raise InvalidGeometryError("Null geometry supports no operations")
532        size = c_size_t()
533        result = self._lgeos.GEOSWKBWriter_writeHEX(
534            self._writer, geom._geom, pointer(size))
535        data = string_at(result, size.value)
536        lgeos.GEOSFree(result)
537        return data.decode('ascii')
538
539
540# Errcheck functions for ctypes
541
542def errcheck_wkb(result, func, argtuple):
543    """Returns bytes from a C pointer"""
544    if not result:
545        return None
546    size_ref = argtuple[-1]
547    size = size_ref.contents
548    retval = string_at(result, size.value)[:]
549    lgeos.GEOSFree(result)
550    return retval
551
552
553def errcheck_just_free(result, func, argtuple):
554    """Returns string from a C pointer"""
555    retval = string_at(result)
556    lgeos.GEOSFree(result)
557    return retval.decode('ascii')
558
559
560def errcheck_null_exception(result, func, argtuple):
561    """Wraps errcheck_just_free
562
563    Raises TopologicalError if result is NULL.
564    """
565    if not result:
566        raise TopologicalError(
567            "The operation '{}' could not be performed."
568            "Likely cause is invalidity of the geometry.".format(
569                func.__name__))
570    return errcheck_just_free(result, func, argtuple)
571
572
573def errcheck_predicate(result, func, argtuple):
574    """Result is 2 on exception, 1 on True, 0 on False"""
575    if result == 2:
576        raise PredicateError("Failed to evaluate %s" % repr(func))
577    return result
578
579
580class LGEOSBase(threading.local):
581    """Proxy for GEOS C API
582
583    This is a base class. Do not instantiate.
584    """
585    methods = {}
586
587    def __init__(self, dll):
588        self._lgeos = dll
589        self.geos_handle = None
590
591    def __del__(self):
592        """Cleanup GEOS related processes"""
593        if self._lgeos is not None:
594            self._lgeos.finishGEOS()
595            self._lgeos = None
596            self.geos_handle = None
597
598
599class LGEOS330(LGEOSBase):
600    """Proxy for GEOS 3.3.0-CAPI-1.7.0
601    """
602    geos_version = (3, 3, 0)
603    geos_capi_version = (1, 7, 0)
604
605    def __init__(self, dll):
606        super().__init__(dll)
607        self.geos_handle = self._lgeos.initGEOS_r(notice_h, error_h)
608        keys = list(self._lgeos.__dict__.keys())
609        for key in [x for x in keys if not x.endswith('_r')]:
610            if key + '_r' in keys:
611                reentr_func = getattr(self._lgeos, key + '_r')
612                attr = partial(reentr_func, self.geos_handle)
613                attr.__name__ = reentr_func.__name__
614                setattr(self, key, attr)
615            else:
616                setattr(self, key, getattr(self._lgeos, key))
617
618        # GEOS 3.3.8 from homebrew has, but doesn't advertise
619        # GEOSPolygonize_full. We patch it in explicitly here.
620        key = 'GEOSPolygonize_full'
621        func = getattr(self._lgeos, key + '_r')
622        attr = partial(func, self.geos_handle)
623        attr.__name__ = func.__name__
624        setattr(self, key, attr)
625
626        # Deprecated
627        self.GEOSGeomToWKB_buf.func.errcheck = errcheck_wkb
628        self.GEOSGeomToWKT.func.errcheck = errcheck_just_free
629        self.GEOSRelate.func.errcheck = errcheck_null_exception
630        for pred in (
631                self.GEOSDisjoint,
632                self.GEOSTouches,
633                self.GEOSIntersects,
634                self.GEOSCrosses,
635                self.GEOSWithin,
636                self.GEOSContains,
637                self.GEOSOverlaps,
638                self.GEOSCovers,
639                self.GEOSEquals,
640                self.GEOSEqualsExact,
641                self.GEOSPreparedDisjoint,
642                self.GEOSPreparedTouches,
643                self.GEOSPreparedCrosses,
644                self.GEOSPreparedWithin,
645                self.GEOSPreparedOverlaps,
646                self.GEOSPreparedContains,
647                self.GEOSPreparedContainsProperly,
648                self.GEOSPreparedCovers,
649                self.GEOSPreparedIntersects,
650                self.GEOSRelatePattern,
651                self.GEOSisEmpty,
652                self.GEOSisValid,
653                self.GEOSisSimple,
654                self.GEOSisRing,
655                self.GEOSHasZ,
656                self.GEOSisClosed,
657                self.GEOSCoveredBy):
658            pred.func.errcheck = errcheck_predicate
659
660        self.GEOSisValidReason.func.errcheck = errcheck_just_free
661
662        self.methods['area'] = self.GEOSArea
663        self.methods['boundary'] = self.GEOSBoundary
664        self.methods['buffer'] = self.GEOSBuffer
665        self.methods['centroid'] = self.GEOSGetCentroid
666        self.methods['representative_point'] = self.GEOSPointOnSurface
667        self.methods['convex_hull'] = self.GEOSConvexHull
668        self.methods['distance'] = self.GEOSDistance
669        self.methods['envelope'] = self.GEOSEnvelope
670        self.methods['length'] = self.GEOSLength
671        self.methods['has_z'] = self.GEOSHasZ
672        self.methods['is_empty'] = self.GEOSisEmpty
673        self.methods['is_ring'] = self.GEOSisRing
674        self.methods['is_simple'] = self.GEOSisSimple
675        self.methods['is_valid'] = self.GEOSisValid
676        self.methods['disjoint'] = self.GEOSDisjoint
677        self.methods['touches'] = self.GEOSTouches
678        self.methods['intersects'] = self.GEOSIntersects
679        self.methods['crosses'] = self.GEOSCrosses
680        self.methods['within'] = self.GEOSWithin
681        self.methods['contains'] = self.GEOSContains
682        self.methods['overlaps'] = self.GEOSOverlaps
683        self.methods['covers'] = self.GEOSCovers
684        self.methods['equals'] = self.GEOSEquals
685        self.methods['equals_exact'] = self.GEOSEqualsExact
686        self.methods['relate'] = self.GEOSRelate
687        self.methods['difference'] = self.GEOSDifference
688        self.methods['symmetric_difference'] = self.GEOSSymDifference
689        self.methods['union'] = self.GEOSUnion
690        self.methods['intersection'] = self.GEOSIntersection
691        self.methods['prepared_disjoint'] = self.GEOSPreparedDisjoint
692        self.methods['prepared_touches'] = self.GEOSPreparedTouches
693        self.methods['prepared_intersects'] = self.GEOSPreparedIntersects
694        self.methods['prepared_crosses'] = self.GEOSPreparedCrosses
695        self.methods['prepared_within'] = self.GEOSPreparedWithin
696        self.methods['prepared_contains'] = self.GEOSPreparedContains
697        self.methods['prepared_contains_properly'] = \
698            self.GEOSPreparedContainsProperly
699        self.methods['prepared_overlaps'] = self.GEOSPreparedOverlaps
700        self.methods['prepared_covers'] = self.GEOSPreparedCovers
701        self.methods['relate_pattern'] = self.GEOSRelatePattern
702        self.methods['simplify'] = self.GEOSSimplify
703        self.methods['topology_preserve_simplify'] = \
704            self.GEOSTopologyPreserveSimplify
705        self.methods['normalize'] = self.GEOSNormalize
706        self.methods['cascaded_union'] = self.GEOSUnionCascaded
707
708        def parallel_offset(geom, distance, resolution=16, join_style=1,
709                            mitre_limit=5.0, side='right'):
710            if side == 'right':
711                distance *= -1
712            return self.GEOSOffsetCurve(
713                geom, distance, resolution, join_style, mitre_limit)
714
715        self.methods['parallel_offset'] = parallel_offset
716        self.methods['project'] = self.GEOSProject
717        self.methods['project_normalized'] = self.GEOSProjectNormalized
718        self.methods['interpolate'] = self.GEOSInterpolate
719        self.methods['interpolate_normalized'] = \
720            self.GEOSInterpolateNormalized
721        self.methods['buffer_with_style'] = self.GEOSBufferWithStyle
722        self.methods['hausdorff_distance'] = self.GEOSHausdorffDistance
723        self.methods['unary_union'] = self.GEOSUnaryUnion
724        self.methods['cascaded_union'] = self.methods['unary_union']
725        self.methods['is_closed'] = self.GEOSisClosed
726        self.methods['snap'] = self.GEOSSnap
727        self.methods['shared_paths'] = self.GEOSSharedPaths
728        self.methods['buffer_with_params'] = self.GEOSBufferWithParams
729        self.methods['covered_by'] = self.GEOSCoveredBy
730
731
732class LGEOS340(LGEOS330):
733    """Proxy for GEOS 3.4.0-CAPI-1.8.0
734    """
735    geos_version = (3, 4, 0)
736    geos_capi_version = (1, 8, 0)
737
738    def __init__(self, dll):
739        super().__init__(dll)
740        self.methods['delaunay_triangulation'] = self.GEOSDelaunayTriangulation
741        self.methods['nearest_points'] = self.GEOSNearestPoints
742
743
744class LGEOS350(LGEOS340):
745    """Proxy for GEOS 3.5.0-CAPI-1.9.0
746    """
747    geos_version = (3, 5, 0)
748    geos_capi_version = (1, 9, 0)
749
750    def __init__(self, dll):
751        super().__init__(dll)
752        self.methods['clip_by_rect'] = self.GEOSClipByRect
753        self.methods['voronoi_diagram'] = self.GEOSVoronoiDiagram
754
755
756class LGEOS360(LGEOS350):
757    """Proxy for GEOS 3.6.0-CAPI-1.10.0
758    """
759    geos_version = (3, 6, 0)
760    geos_capi_version = (1, 10, 0)
761
762    def __init__(self, dll):
763        super().__init__(dll)
764        self.methods['minimum_clearance'] = self.GEOSMinimumClearance
765
766
767class LGEOS380(LGEOS360):
768    """Proxy for GEOS 3.8.0-CAPI-1.13.0"""
769
770    geos_version = (3, 8, 0)
771    geos_capi_version = (1, 13, 0)
772
773    def __init__(self, dll):
774        super().__init__(dll)
775        self.methods['make_valid'] = self.GEOSMakeValid
776
777
778if geos_version >= (3, 8, 0):
779    L = LGEOS380
780elif geos_version >= (3, 6, 0):
781    L = LGEOS360
782elif geos_version >= (3, 5, 0):
783    L = LGEOS350
784elif geos_version >= (3, 4, 0):
785    L = LGEOS340
786elif geos_version >= (3, 3, 0):
787    L = LGEOS330
788else:
789    raise ValueError('unexpected geos_version: ' + str(geos_version))
790
791lgeos = L(_lgeos)
792
793
794def cleanup(proxy):
795    del proxy
796
797atexit.register(cleanup, lgeos)
798