geometry.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. """
  2. This module contains the 'base' GEOSGeometry object -- all GEOS Geometries
  3. inherit from this object.
  4. """
  5. import json
  6. from ctypes import addressof, byref, c_double
  7. from django.contrib.gis import gdal
  8. from django.contrib.gis.geometry.regex import hex_regex, json_regex, wkt_regex
  9. from django.contrib.gis.geos import prototypes as capi
  10. from django.contrib.gis.geos.base import GEOSBase
  11. from django.contrib.gis.geos.coordseq import GEOSCoordSeq
  12. from django.contrib.gis.geos.error import GEOSException
  13. from django.contrib.gis.geos.libgeos import GEOM_PTR
  14. from django.contrib.gis.geos.mutable_list import ListMixin
  15. from django.contrib.gis.geos.prepared import PreparedGeometry
  16. from django.contrib.gis.geos.prototypes.io import (
  17. ewkb_w, wkb_r, wkb_w, wkt_r, wkt_w,
  18. )
  19. from django.utils.deconstruct import deconstructible
  20. from django.utils.encoding import force_bytes, force_text
  21. @deconstructible
  22. class GEOSGeometry(GEOSBase, ListMixin):
  23. "A class that, generally, encapsulates a GEOS geometry."
  24. _GEOS_CLASSES = None
  25. ptr_type = GEOM_PTR
  26. destructor = capi.destroy_geom
  27. has_cs = False # Only Point, LineString, LinearRing have coordinate sequences
  28. def __init__(self, geo_input, srid=None):
  29. """
  30. The base constructor for GEOS geometry objects, and may take the
  31. following inputs:
  32. * strings:
  33. - WKT
  34. - HEXEWKB (a PostGIS-specific canonical form)
  35. - GeoJSON (requires GDAL)
  36. * buffer:
  37. - WKB
  38. The `srid` keyword is used to specify the Source Reference Identifier
  39. (SRID) number for this Geometry. If not set, the SRID will be None.
  40. """
  41. if isinstance(geo_input, bytes):
  42. geo_input = force_text(geo_input)
  43. if isinstance(geo_input, str):
  44. wkt_m = wkt_regex.match(geo_input)
  45. if wkt_m:
  46. # Handling WKT input.
  47. if wkt_m.group('srid'):
  48. srid = int(wkt_m.group('srid'))
  49. g = wkt_r().read(force_bytes(wkt_m.group('wkt')))
  50. elif hex_regex.match(geo_input):
  51. # Handling HEXEWKB input.
  52. g = wkb_r().read(force_bytes(geo_input))
  53. elif json_regex.match(geo_input):
  54. # Handling GeoJSON input.
  55. g = wkb_r().read(gdal.OGRGeometry(geo_input).wkb)
  56. else:
  57. raise ValueError('String input unrecognized as WKT EWKT, and HEXEWKB.')
  58. elif isinstance(geo_input, GEOM_PTR):
  59. # When the input is a pointer to a geometry (GEOM_PTR).
  60. g = geo_input
  61. elif isinstance(geo_input, memoryview):
  62. # When the input is a buffer (WKB).
  63. g = wkb_r().read(geo_input)
  64. elif isinstance(geo_input, GEOSGeometry):
  65. g = capi.geom_clone(geo_input.ptr)
  66. else:
  67. # Invalid geometry type.
  68. raise TypeError('Improper geometry input type: %s' % type(geo_input))
  69. if g:
  70. # Setting the pointer object with a valid pointer.
  71. self.ptr = g
  72. else:
  73. raise GEOSException('Could not initialize GEOS Geometry with given input.')
  74. # Post-initialization setup.
  75. self._post_init(srid)
  76. def _post_init(self, srid):
  77. "Helper routine for performing post-initialization setup."
  78. # Setting the SRID, if given.
  79. if srid and isinstance(srid, int):
  80. self.srid = srid
  81. # Setting the class type (e.g., Point, Polygon, etc.)
  82. if type(self) == GEOSGeometry:
  83. if GEOSGeometry._GEOS_CLASSES is None:
  84. # Lazy-loaded variable to avoid import conflicts with GEOSGeometry.
  85. from .linestring import LineString, LinearRing
  86. from .point import Point
  87. from .polygon import Polygon
  88. from .collections import (
  89. GeometryCollection, MultiPoint, MultiLineString, MultiPolygon,
  90. )
  91. GEOSGeometry._GEOS_CLASSES = {
  92. 0: Point,
  93. 1: LineString,
  94. 2: LinearRing,
  95. 3: Polygon,
  96. 4: MultiPoint,
  97. 5: MultiLineString,
  98. 6: MultiPolygon,
  99. 7: GeometryCollection,
  100. }
  101. self.__class__ = GEOSGeometry._GEOS_CLASSES[self.geom_typeid]
  102. # Setting the coordinate sequence for the geometry (will be None on
  103. # geometries that do not have coordinate sequences)
  104. self._set_cs()
  105. def __copy__(self):
  106. """
  107. Returns a clone because the copy of a GEOSGeometry may contain an
  108. invalid pointer location if the original is garbage collected.
  109. """
  110. return self.clone()
  111. def __deepcopy__(self, memodict):
  112. """
  113. The `deepcopy` routine is used by the `Node` class of django.utils.tree;
  114. thus, the protocol routine needs to be implemented to return correct
  115. copies (clones) of these GEOS objects, which use C pointers.
  116. """
  117. return self.clone()
  118. def __str__(self):
  119. "EWKT is used for the string representation."
  120. return self.ewkt
  121. def __repr__(self):
  122. "Short-hand representation because WKT may be very large."
  123. return '<%s object at %s>' % (self.geom_type, hex(addressof(self.ptr)))
  124. # Pickling support
  125. def __getstate__(self):
  126. # The pickled state is simply a tuple of the WKB (in string form)
  127. # and the SRID.
  128. return bytes(self.wkb), self.srid
  129. def __setstate__(self, state):
  130. # Instantiating from the tuple state that was pickled.
  131. wkb, srid = state
  132. ptr = wkb_r().read(memoryview(wkb))
  133. if not ptr:
  134. raise GEOSException('Invalid Geometry loaded from pickled state.')
  135. self.ptr = ptr
  136. self._post_init(srid)
  137. @classmethod
  138. def _from_wkb(cls, wkb):
  139. return wkb_r().read(wkb)
  140. @classmethod
  141. def from_gml(cls, gml_string):
  142. return gdal.OGRGeometry.from_gml(gml_string).geos
  143. # Comparison operators
  144. def __eq__(self, other):
  145. """
  146. Equivalence testing, a Geometry may be compared with another Geometry
  147. or an EWKT representation.
  148. """
  149. if isinstance(other, str):
  150. if other.startswith('SRID=0;'):
  151. return self.ewkt == other[7:] # Test only WKT part of other
  152. return self.ewkt == other
  153. elif isinstance(other, GEOSGeometry):
  154. return self.srid == other.srid and self.equals_exact(other)
  155. else:
  156. return False
  157. # ### Geometry set-like operations ###
  158. # Thanks to Sean Gillies for inspiration:
  159. # http://lists.gispython.org/pipermail/community/2007-July/001034.html
  160. # g = g1 | g2
  161. def __or__(self, other):
  162. "Returns the union of this Geometry and the other."
  163. return self.union(other)
  164. # g = g1 & g2
  165. def __and__(self, other):
  166. "Returns the intersection of this Geometry and the other."
  167. return self.intersection(other)
  168. # g = g1 - g2
  169. def __sub__(self, other):
  170. "Return the difference this Geometry and the other."
  171. return self.difference(other)
  172. # g = g1 ^ g2
  173. def __xor__(self, other):
  174. "Return the symmetric difference of this Geometry and the other."
  175. return self.sym_difference(other)
  176. # #### Coordinate Sequence Routines ####
  177. def _set_cs(self):
  178. "Sets the coordinate sequence for this Geometry."
  179. if self.has_cs:
  180. self._cs = GEOSCoordSeq(capi.get_cs(self.ptr), self.hasz)
  181. else:
  182. self._cs = None
  183. @property
  184. def coord_seq(self):
  185. "Returns a clone of the coordinate sequence for this Geometry."
  186. if self.has_cs:
  187. return self._cs.clone()
  188. # #### Geometry Info ####
  189. @property
  190. def geom_type(self):
  191. "Returns a string representing the Geometry type, e.g. 'Polygon'"
  192. return capi.geos_type(self.ptr).decode()
  193. @property
  194. def geom_typeid(self):
  195. "Returns an integer representing the Geometry type."
  196. return capi.geos_typeid(self.ptr)
  197. @property
  198. def num_geom(self):
  199. "Returns the number of geometries in the Geometry."
  200. return capi.get_num_geoms(self.ptr)
  201. @property
  202. def num_coords(self):
  203. "Returns the number of coordinates in the Geometry."
  204. return capi.get_num_coords(self.ptr)
  205. @property
  206. def num_points(self):
  207. "Returns the number points, or coordinates, in the Geometry."
  208. return self.num_coords
  209. @property
  210. def dims(self):
  211. "Returns the dimension of this Geometry (0=point, 1=line, 2=surface)."
  212. return capi.get_dims(self.ptr)
  213. def normalize(self):
  214. "Converts this Geometry to normal form (or canonical form)."
  215. capi.geos_normalize(self.ptr)
  216. # #### Unary predicates ####
  217. @property
  218. def empty(self):
  219. """
  220. Returns a boolean indicating whether the set of points in this Geometry
  221. are empty.
  222. """
  223. return capi.geos_isempty(self.ptr)
  224. @property
  225. def hasz(self):
  226. "Returns whether the geometry has a 3D dimension."
  227. return capi.geos_hasz(self.ptr)
  228. @property
  229. def ring(self):
  230. "Returns whether or not the geometry is a ring."
  231. return capi.geos_isring(self.ptr)
  232. @property
  233. def simple(self):
  234. "Returns false if the Geometry not simple."
  235. return capi.geos_issimple(self.ptr)
  236. @property
  237. def valid(self):
  238. "This property tests the validity of this Geometry."
  239. return capi.geos_isvalid(self.ptr)
  240. @property
  241. def valid_reason(self):
  242. """
  243. Returns a string containing the reason for any invalidity.
  244. """
  245. return capi.geos_isvalidreason(self.ptr).decode()
  246. # #### Binary predicates. ####
  247. def contains(self, other):
  248. "Returns true if other.within(this) returns true."
  249. return capi.geos_contains(self.ptr, other.ptr)
  250. def covers(self, other):
  251. """
  252. Return True if the DE-9IM Intersection Matrix for the two geometries is
  253. T*****FF*, *T****FF*, ***T**FF*, or ****T*FF*. If either geometry is
  254. empty, return False.
  255. """
  256. return capi.geos_covers(self.ptr, other.ptr)
  257. def crosses(self, other):
  258. """
  259. Returns true if the DE-9IM intersection matrix for the two Geometries
  260. is T*T****** (for a point and a curve,a point and an area or a line and
  261. an area) 0******** (for two curves).
  262. """
  263. return capi.geos_crosses(self.ptr, other.ptr)
  264. def disjoint(self, other):
  265. """
  266. Returns true if the DE-9IM intersection matrix for the two Geometries
  267. is FF*FF****.
  268. """
  269. return capi.geos_disjoint(self.ptr, other.ptr)
  270. def equals(self, other):
  271. """
  272. Returns true if the DE-9IM intersection matrix for the two Geometries
  273. is T*F**FFF*.
  274. """
  275. return capi.geos_equals(self.ptr, other.ptr)
  276. def equals_exact(self, other, tolerance=0):
  277. """
  278. Returns true if the two Geometries are exactly equal, up to a
  279. specified tolerance.
  280. """
  281. return capi.geos_equalsexact(self.ptr, other.ptr, float(tolerance))
  282. def intersects(self, other):
  283. "Returns true if disjoint returns false."
  284. return capi.geos_intersects(self.ptr, other.ptr)
  285. def overlaps(self, other):
  286. """
  287. Returns true if the DE-9IM intersection matrix for the two Geometries
  288. is T*T***T** (for two points or two surfaces) 1*T***T** (for two curves).
  289. """
  290. return capi.geos_overlaps(self.ptr, other.ptr)
  291. def relate_pattern(self, other, pattern):
  292. """
  293. Returns true if the elements in the DE-9IM intersection matrix for the
  294. two Geometries match the elements in pattern.
  295. """
  296. if not isinstance(pattern, str) or len(pattern) > 9:
  297. raise GEOSException('invalid intersection matrix pattern')
  298. return capi.geos_relatepattern(self.ptr, other.ptr, force_bytes(pattern))
  299. def touches(self, other):
  300. """
  301. Returns true if the DE-9IM intersection matrix for the two Geometries
  302. is FT*******, F**T***** or F***T****.
  303. """
  304. return capi.geos_touches(self.ptr, other.ptr)
  305. def within(self, other):
  306. """
  307. Returns true if the DE-9IM intersection matrix for the two Geometries
  308. is T*F**F***.
  309. """
  310. return capi.geos_within(self.ptr, other.ptr)
  311. # #### SRID Routines ####
  312. @property
  313. def srid(self):
  314. "Gets the SRID for the geometry, returns None if no SRID is set."
  315. s = capi.geos_get_srid(self.ptr)
  316. if s == 0:
  317. return None
  318. else:
  319. return s
  320. @srid.setter
  321. def srid(self, srid):
  322. "Sets the SRID for the geometry."
  323. capi.geos_set_srid(self.ptr, 0 if srid is None else srid)
  324. # #### Output Routines ####
  325. @property
  326. def ewkt(self):
  327. """
  328. Returns the EWKT (SRID + WKT) of the Geometry.
  329. """
  330. srid = self.srid
  331. return 'SRID=%s;%s' % (srid, self.wkt) if srid else self.wkt
  332. @property
  333. def wkt(self):
  334. "Returns the WKT (Well-Known Text) representation of this Geometry."
  335. return wkt_w(dim=3 if self.hasz else 2, trim=True).write(self).decode()
  336. @property
  337. def hex(self):
  338. """
  339. Returns the WKB of this Geometry in hexadecimal form. Please note
  340. that the SRID is not included in this representation because it is not
  341. a part of the OGC specification (use the `hexewkb` property instead).
  342. """
  343. # A possible faster, all-python, implementation:
  344. # str(self.wkb).encode('hex')
  345. return wkb_w(dim=3 if self.hasz else 2).write_hex(self)
  346. @property
  347. def hexewkb(self):
  348. """
  349. Returns the EWKB of this Geometry in hexadecimal form. This is an
  350. extension of the WKB specification that includes SRID value that are
  351. a part of this geometry.
  352. """
  353. return ewkb_w(dim=3 if self.hasz else 2).write_hex(self)
  354. @property
  355. def json(self):
  356. """
  357. Returns GeoJSON representation of this Geometry.
  358. """
  359. return json.dumps({'type': self.__class__.__name__, 'coordinates': self.coords})
  360. geojson = json
  361. @property
  362. def wkb(self):
  363. """
  364. Returns the WKB (Well-Known Binary) representation of this Geometry
  365. as a Python buffer. SRID and Z values are not included, use the
  366. `ewkb` property instead.
  367. """
  368. return wkb_w(3 if self.hasz else 2).write(self)
  369. @property
  370. def ewkb(self):
  371. """
  372. Return the EWKB representation of this Geometry as a Python buffer.
  373. This is an extension of the WKB specification that includes any SRID
  374. value that are a part of this geometry.
  375. """
  376. return ewkb_w(3 if self.hasz else 2).write(self)
  377. @property
  378. def kml(self):
  379. "Returns the KML representation of this Geometry."
  380. gtype = self.geom_type
  381. return '<%s>%s</%s>' % (gtype, self.coord_seq.kml, gtype)
  382. @property
  383. def prepared(self):
  384. """
  385. Returns a PreparedGeometry corresponding to this geometry -- it is
  386. optimized for the contains, intersects, and covers operations.
  387. """
  388. return PreparedGeometry(self)
  389. # #### GDAL-specific output routines ####
  390. def _ogr_ptr(self):
  391. return gdal.OGRGeometry._from_wkb(self.wkb)
  392. @property
  393. def ogr(self):
  394. "Returns the OGR Geometry for this Geometry."
  395. return gdal.OGRGeometry(self._ogr_ptr(), self.srs)
  396. @property
  397. def srs(self):
  398. "Returns the OSR SpatialReference for SRID of this Geometry."
  399. if self.srid:
  400. try:
  401. return gdal.SpatialReference(self.srid)
  402. except gdal.SRSException:
  403. pass
  404. return None
  405. @property
  406. def crs(self):
  407. "Alias for `srs` property."
  408. return self.srs
  409. def transform(self, ct, clone=False):
  410. """
  411. Requires GDAL. Transforms the geometry according to the given
  412. transformation object, which may be an integer SRID, and WKT or
  413. PROJ.4 string. By default, the geometry is transformed in-place and
  414. nothing is returned. However if the `clone` keyword is set, then this
  415. geometry will not be modified and a transformed clone will be returned
  416. instead.
  417. """
  418. srid = self.srid
  419. if ct == srid:
  420. # short-circuit where source & dest SRIDs match
  421. if clone:
  422. return self.clone()
  423. else:
  424. return
  425. if isinstance(ct, gdal.CoordTransform):
  426. # We don't care about SRID because CoordTransform presupposes
  427. # source SRS.
  428. srid = None
  429. elif srid is None or srid < 0:
  430. raise GEOSException("Calling transform() with no SRID set is not supported")
  431. # Creating an OGR Geometry, which is then transformed.
  432. g = gdal.OGRGeometry(self._ogr_ptr(), srid)
  433. g.transform(ct)
  434. # Getting a new GEOS pointer
  435. ptr = g._geos_ptr()
  436. if clone:
  437. # User wants a cloned transformed geometry returned.
  438. return GEOSGeometry(ptr, srid=g.srid)
  439. if ptr:
  440. # Reassigning pointer, and performing post-initialization setup
  441. # again due to the reassignment.
  442. capi.destroy_geom(self.ptr)
  443. self.ptr = ptr
  444. self._post_init(g.srid)
  445. else:
  446. raise GEOSException('Transformed WKB was invalid.')
  447. # #### Topology Routines ####
  448. def _topology(self, gptr):
  449. "Helper routine to return Geometry from the given pointer."
  450. return GEOSGeometry(gptr, srid=self.srid)
  451. @property
  452. def boundary(self):
  453. "Returns the boundary as a newly allocated Geometry object."
  454. return self._topology(capi.geos_boundary(self.ptr))
  455. def buffer(self, width, quadsegs=8):
  456. """
  457. Returns a geometry that represents all points whose distance from this
  458. Geometry is less than or equal to distance. Calculations are in the
  459. Spatial Reference System of this Geometry. The optional third parameter sets
  460. the number of segment used to approximate a quarter circle (defaults to 8).
  461. (Text from PostGIS documentation at ch. 6.1.3)
  462. """
  463. return self._topology(capi.geos_buffer(self.ptr, width, quadsegs))
  464. @property
  465. def centroid(self):
  466. """
  467. The centroid is equal to the centroid of the set of component Geometries
  468. of highest dimension (since the lower-dimension geometries contribute zero
  469. "weight" to the centroid).
  470. """
  471. return self._topology(capi.geos_centroid(self.ptr))
  472. @property
  473. def convex_hull(self):
  474. """
  475. Returns the smallest convex Polygon that contains all the points
  476. in the Geometry.
  477. """
  478. return self._topology(capi.geos_convexhull(self.ptr))
  479. def difference(self, other):
  480. """
  481. Returns a Geometry representing the points making up this Geometry
  482. that do not make up other.
  483. """
  484. return self._topology(capi.geos_difference(self.ptr, other.ptr))
  485. @property
  486. def envelope(self):
  487. "Return the envelope for this geometry (a polygon)."
  488. return self._topology(capi.geos_envelope(self.ptr))
  489. def intersection(self, other):
  490. "Returns a Geometry representing the points shared by this Geometry and other."
  491. return self._topology(capi.geos_intersection(self.ptr, other.ptr))
  492. @property
  493. def point_on_surface(self):
  494. "Computes an interior point of this Geometry."
  495. return self._topology(capi.geos_pointonsurface(self.ptr))
  496. def relate(self, other):
  497. "Returns the DE-9IM intersection matrix for this Geometry and the other."
  498. return capi.geos_relate(self.ptr, other.ptr).decode()
  499. def simplify(self, tolerance=0.0, preserve_topology=False):
  500. """
  501. Returns the Geometry, simplified using the Douglas-Peucker algorithm
  502. to the specified tolerance (higher tolerance => less points). If no
  503. tolerance provided, defaults to 0.
  504. By default, this function does not preserve topology - e.g. polygons can
  505. be split, collapse to lines or disappear holes can be created or
  506. disappear, and lines can cross. By specifying preserve_topology=True,
  507. the result will have the same dimension and number of components as the
  508. input. This is significantly slower.
  509. """
  510. if preserve_topology:
  511. return self._topology(capi.geos_preservesimplify(self.ptr, tolerance))
  512. else:
  513. return self._topology(capi.geos_simplify(self.ptr, tolerance))
  514. def sym_difference(self, other):
  515. """
  516. Returns a set combining the points in this Geometry not in other,
  517. and the points in other not in this Geometry.
  518. """
  519. return self._topology(capi.geos_symdifference(self.ptr, other.ptr))
  520. @property
  521. def unary_union(self):
  522. "Return the union of all the elements of this geometry."
  523. return self._topology(capi.geos_unary_union(self.ptr))
  524. def union(self, other):
  525. "Returns a Geometry representing all the points in this Geometry and other."
  526. return self._topology(capi.geos_union(self.ptr, other.ptr))
  527. # #### Other Routines ####
  528. @property
  529. def area(self):
  530. "Returns the area of the Geometry."
  531. return capi.geos_area(self.ptr, byref(c_double()))
  532. def distance(self, other):
  533. """
  534. Returns the distance between the closest points on this Geometry
  535. and the other. Units will be in those of the coordinate system of
  536. the Geometry.
  537. """
  538. if not isinstance(other, GEOSGeometry):
  539. raise TypeError('distance() works only on other GEOS Geometries.')
  540. return capi.geos_distance(self.ptr, other.ptr, byref(c_double()))
  541. @property
  542. def extent(self):
  543. """
  544. Returns the extent of this geometry as a 4-tuple, consisting of
  545. (xmin, ymin, xmax, ymax).
  546. """
  547. from .point import Point
  548. env = self.envelope
  549. if isinstance(env, Point):
  550. xmin, ymin = env.tuple
  551. xmax, ymax = xmin, ymin
  552. else:
  553. xmin, ymin = env[0][0]
  554. xmax, ymax = env[0][2]
  555. return (xmin, ymin, xmax, ymax)
  556. @property
  557. def length(self):
  558. """
  559. Returns the length of this Geometry (e.g., 0 for point, or the
  560. circumference of a Polygon).
  561. """
  562. return capi.geos_length(self.ptr, byref(c_double()))
  563. def clone(self):
  564. "Clones this Geometry."
  565. return GEOSGeometry(capi.geom_clone(self.ptr), srid=self.srid)
  566. class LinearGeometryMixin:
  567. """
  568. Used for LineString and MultiLineString.
  569. """
  570. def interpolate(self, distance):
  571. return self._topology(capi.geos_interpolate(self.ptr, distance))
  572. def interpolate_normalized(self, distance):
  573. return self._topology(capi.geos_interpolate_normalized(self.ptr, distance))
  574. def project(self, point):
  575. from .point import Point
  576. if not isinstance(point, Point):
  577. raise TypeError('locate_point argument must be a Point')
  578. return capi.geos_project(self.ptr, point.ptr)
  579. def project_normalized(self, point):
  580. from .point import Point
  581. if not isinstance(point, Point):
  582. raise TypeError('locate_point argument must be a Point')
  583. return capi.geos_project_normalized(self.ptr, point.ptr)
  584. @property
  585. def merged(self):
  586. """
  587. Return the line merge of this Geometry.
  588. """
  589. return self._topology(capi.geos_linemerge(self.ptr))
  590. @property
  591. def closed(self):
  592. """
  593. Return whether or not this Geometry is closed.
  594. """
  595. return capi.geos_isclosed(self.ptr)