Coverage for pygeodesy/frechet.py: 96%
205 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-10 16:55 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-10 16:55 -0500
2# -*- coding: utf-8 -*-
4u'''Fréchet distances.
6Classes L{Frechet}, L{FrechetDegrees}, L{FrechetRadians}, L{FrechetCosineLaw},
7L{FrechetDistanceTo}, L{FrechetEquirectangular}, L{FrechetEuclidean},
8L{FrechetExact}, L{FrechetFlatLocal}, L{FrechetFlatPolar}, L{FrechetHaversine},
9L{FrechetHubeny}, L{FrechetKarney}, L{FrechetThomas} and L{FrechetVincentys}
10to compute I{discrete} U{Fréchet<https://WikiPedia.org/wiki/Frechet_distance>}
11distances between two sets of C{LatLon}, C{NumPy}, C{tuples} or other types of points.
13Only L{FrechetDistanceTo} -iff used with L{ellipsoidalKarney.LatLon}
14points- and L{FrechetKarney} requires installation of I{Charles Karney}'s
15U{geographiclib<https://PyPI.org/project/geographiclib>}.
17Typical usage is as follows. First, create a C{Frechet} calculator from one
18set of C{LatLon} points.
20C{f = FrechetXyz(point1s, ...)}
22Get the I{discrete} Fréchet distance to another set of C{LatLon} points
23by
25C{t6 = f.discrete(point2s)}
27Or, use function C{frechet_} with a proper C{distance} function passed
28as keyword arguments as follows
30C{t6 = frechet_(point1s, point2s, ..., distance=...)}.
32In both cases, the returned result C{t6} is a L{Frechet6Tuple}.
34For C{(lat, lon, ...)} points in a C{NumPy} array or plain C{tuples},
35wrap the points in a L{Numpy2LatLon} respectively L{Tuple2LatLon}
36instance, more details in the documentation thereof.
38For other points, create a L{Frechet} sub-class with the appropriate
39C{distance} method overloading L{Frechet.distance} as in this example.
41 >>> from pygeodesy import Frechet, hypot_
42 >>>
43 >>> class F3D(Frechet):
44 >>> """Custom Frechet example.
45 >>> """
46 >>> def distance(self, p1, p2):
47 >>> return hypot_(p1.x - p2.x, p1.y - p2.y, p1.z - p2.z)
48 >>>
49 >>> f3D = F3D(xyz1, ..., units="...")
50 >>> t6 = f3D.discrete(xyz2)
52Transcribed from the original U{Computing Discrete Fréchet Distance
53<https://www.kr.TUWien.ac.AT/staff/eiter/et-archive/cdtr9464.pdf>} by
54Eiter, T. and Mannila, H., 1994, April 25, Technical Report CD-TR 94/64,
55Information Systems Department/Christian Doppler Laboratory for Expert
56Systems, Technical University Vienna, Austria.
58This L{Frechet.discrete} implementation optionally generates intermediate
59points for each point set separately. For example, using keyword argument
60C{fraction=0.5} adds one additional point halfway between each pair of
61points. Or using C{fraction=0.1} interpolates nine additional points
62between each points pair.
64The L{Frechet6Tuple} attributes C{fi1} and/or C{fi2} will be I{fractional}
65indices of type C{float} if keyword argument C{fraction} is used. Otherwise,
66C{fi1} and/or C{fi2} are simply type C{int} indices into the respective
67points set.
69For example, C{fractional} index value 2.5 means an intermediate point
70halfway between points[2] and points[3]. Use function L{fractional}
71to obtain the intermediate point for a I{fractional} index in the
72corresponding set of points.
74The C{Fréchet} distance was introduced in 1906 by U{Maurice Fréchet
75<https://WikiPedia.org/wiki/Maurice_Rene_Frechet>}, see U{reference
76[6]<https://www.kr.TUWien.ac.AT/staff/eiter/et-archive/cdtr9464.pdf>}.
77It is a measure of similarity between curves that takes into account the
78location and ordering of the points. Therefore, it is often a better metric
79than the well-known C{Hausdorff} distance, see the L{hausdorff} module.
80'''
82# from pygeodesy.basics import isscalar # from .points
83from pygeodesy.constants import EPS, EPS1, INF, NINF
84from pygeodesy.datums import _ellipsoidal_datum, _WGS84
85from pygeodesy.errors import PointsError, _xattr, _xcallable, _xkwds, _xkwds_get
86import pygeodesy.formy as _formy
87from pygeodesy.interns import NN, _DOT_, _n_, _units_
88# from pygeodesy.iters import points2 as _points2 # from .points
89from pygeodesy.lazily import _ALL_LAZY, _FOR_DOCS
90from pygeodesy.named import _name2__, _Named, _NamedTuple, _Pass
91# from pygeodesy.namedTuples import PhiLam2Tuple # from .points
92from pygeodesy.points import _distanceTo, _fractional, isscalar, PhiLam2Tuple, \
93 points2 as _points2, radians
94from pygeodesy.props import Property, property_doc_, property_RO
95from pygeodesy.units import FIx, Float, Number_
96from pygeodesy import unitsBase as _unitsBase # _Str_..., _xUnit, _xUnits
98from collections import defaultdict as _defaultdict
99# from math import radians # from .points
101__all__ = _ALL_LAZY.frechet
102__version__ = '24.12.31'
105def _fraction(fraction, n):
106 f = 1 # int, no fractional indices
107 if fraction in (None, 1):
108 pass
109 elif not (isscalar(fraction) and EPS < fraction < EPS1
110 and (float(n) - fraction) < n):
111 raise FrechetError(fraction=fraction)
112 elif fraction < EPS1:
113 f = float(fraction)
114 return f
117class FrechetError(PointsError):
118 '''Fréchet issue.
119 '''
120 pass
123class Frechet(_Named):
124 '''Frechet base class, requires method L{Frechet.distance} to
125 be overloaded.
126 '''
127 _datum = _WGS84
128 _func = None # formy function/property
129 _f1 = 1
130 _kwds = {} # func_ options
131 _n1 = 0
132 _ps1 = None
133 _units = _unitsBase._Str_NN # XXX Str to _Pass and for backward compatibility
135 def __init__(self, point1s, fraction=None, units=NN, **name__kwds):
136 '''New C{Frechet...} calculator/interpolator.
138 @arg point1s: First set of points (C{LatLon}[], L{Numpy2LatLon}[],
139 L{Tuple2LatLon}[] or C{other}[]).
140 @kwarg fraction: Index fraction (C{float} in L{EPS}..L{EPS1}) to
141 interpolate intermediate B{C{point1s}} or use C{None},
142 C{0} or C{1} for no intermediate B{C{point1s}} and no
143 I{fractional} indices.
144 @kwarg units: Optional distance units (C{Unit} or C{str}).
145 @kwarg name__kwds: Optional C{B{name}=NN} for this calculator/interpolator
146 (C{str}) and any keyword arguments for the distance function,
147 retrievable with property C{kwds}.
149 @raise FrechetError: Insufficient number of B{C{point1s}} or an invalid
150 B{C{point1}}, B{C{fraction}} or B{C{units}}.
151 '''
152 name, kwds = _name2__(**name__kwds) # name__=self.__class__
153 if name:
154 self.name = name
156 self._n1, self._ps1 = self._points2(point1s)
157 if fraction:
158 self.fraction = fraction
159 if units: # and not self.units:
160 self.units = units
161 if kwds:
162 self._kwds = kwds
164 @property_RO
165 def adjust(self):
166 '''Get the C{adjust} setting (C{bool} or C{None}).
167 '''
168 return _xkwds_get(self._kwds, adjust=None)
170 @property_RO
171 def datum(self):
172 '''Get the datum (L{Datum} or C{None} if not applicable).
173 '''
174 return self._datum
176 def _datum_setter(self, datum):
177 '''(INTERNAL) Set the datum.
178 '''
179 d = datum or _xattr(self._ps1[0], datum=None)
180 if d and d is not self._datum: # PYCHOK no cover
181 self._datum = _ellipsoidal_datum(d, name=self.name)
183 def discrete(self, point2s, fraction=None):
184 '''Compute the C{forward, discrete Fréchet} distance.
186 @arg point2s: Second set of points (C{LatLon}[], L{Numpy2LatLon}[],
187 L{Tuple2LatLon}[] or C{other}[]).
188 @kwarg fraction: Index fraction (C{float} in L{EPS}..L{EPS1}) to
189 interpolate intermediate B{C{point2s}} or use
190 C{None}, C{0} or C{1} for no intermediate
191 B{C{point2s}} and no I{fractional} indices.
193 @return: A L{Frechet6Tuple}C{(fd, fi1, fi2, r, n, units)}.
195 @raise FrechetError: Insufficient number of B{C{point2s}} or
196 an invalid B{C{point2}} or B{C{fraction}}.
198 @raise RecursionError: Recursion depth exceeded, see U{sys.getrecursionlimit
199 <https://docs.Python.org/3/library/sys.html#sys.getrecursionlimit>}.
200 '''
201 return self._discrete(point2s, fraction, self.distance)
203 def _discrete(self, point2s, fraction, distance):
204 '''(INTERNAL) Detailed C{discrete} with C{disance}.
205 '''
206 n2, ps2 = self._points2(point2s)
208 f2 = _fraction(fraction, n2)
209 p2 = self.points_fraction if f2 < EPS1 else self.points_ # PYCHOK expected
211 f1 = self.fraction
212 p1 = self.points_fraction if f1 < EPS1 else self.points_ # PYCHOK expected
214 def _dF(fi1, fi2):
215 return distance(p1(self._ps1, fi1), p2(ps2, fi2))
217 try:
218 return _frechet_(self._n1, f1, n2, f2, _dF, self.units)
219 except TypeError as x:
220 t = _DOT_(self.classname, self.discrete.__name__)
221 raise FrechetError(t, cause=x)
223 def distance(self, point1, point2):
224 '''Return the distance between B{C{point1}} and B{C{point2s}},
225 subject to the supplied optional keyword arguments, see
226 property C{kwds}.
227 '''
228 return self._func(point1.lat, point1.lon,
229 point2.lat, point2.lon, **self._kwds)
231 @property_doc_(''' the index fraction (C{float}).''')
232 def fraction(self):
233 '''Get the index fraction (C{float} or C{1}).
234 '''
235 return self._f1
237 @fraction.setter # PYCHOK setter!
238 def fraction(self, fraction):
239 '''Set the index fraction (C{float} in C{EPS}..L{EPS1}) to interpolate
240 intermediate B{C{point1s}} or use C{None}, C{0} or C{1} for no
241 intermediate B{C{point1s}} and no I{fractional} indices.
243 @raise FrechetError: Invalid B{C{fraction}}.
244 '''
245 self._f1 = _fraction(fraction, self._n1)
247# def _func(self, *args, **kwds): # PYCHOK no cover
248# '''(INTERNAL) I{Must be overloaded}.'''
249# self._notOverloaded(*args, **kwds)
251 @Property
252 def _func(self):
253 '''(INTERNAL) I{Must be overloaded}.'''
254 self._notOverloaded(**self.kwds)
256 @_func.setter_ # PYCHOK setter_underscore!
257 def _func(self, func):
258 return _formy._Propy(func, 4, self.kwds)
260 @property_RO
261 def kwds(self):
262 '''Get the supplied, optional keyword arguments (C{dict}).
263 '''
264 return self._kwds
266 def point(self, point):
267 '''Convert a point for the C{.distance} method.
269 @arg point: The point to convert ((C{LatLon}, L{Numpy2LatLon},
270 L{Tuple2LatLon} or C{other}).
272 @return: The converted B{C{point}}.
273 '''
274 return point # pass thru
276 def points_(self, points, i):
277 '''Get and convert a point for the C{.distance} method.
279 @arg points: The orignal B{C{points}} to convert ((C{LatLon}[],
280 L{Numpy2LatLon}[], L{Tuple2LatLon}[] or C{other}[]).
281 @arg i: The B{C{points}} index (C{int}).
283 @return: The converted B{C{points[i]}}.
284 '''
285 return self.point(points[i])
287 def points_fraction(self, points, fi):
288 '''Get and convert a I{fractional} point for the C{.distance} method.
290 @arg points: The orignal B{C{points}} to convert ((C{LatLon}[],
291 L{Numpy2LatLon}[], L{Tuple2LatLon}[] or C{other}[]).
292 @arg fi: The I{fractional} index in B{C{points}} (C{float} or C{int}).
294 @return: The interpolated, converted, intermediate B{C{points[fi]}}.
295 '''
296 return self.point(_fractional(points, fi, None, wrap=None)) # was=self.wrap
298 def _points2(self, points):
299 '''(INTERNAL) Check a set of points, overloaded in L{FrechetDistanceTo}.
300 '''
301 return _points2(points, closed=False, Error=FrechetError)
303 @property_doc_(''' the distance units (C{Unit} or C{str}).''')
304 def units(self):
305 '''Get the distance units (C{Unit} or C{str}).
306 '''
307 return self._units
309 @units.setter # PYCHOK setter!
310 def units(self, units):
311 '''Set the distance units (C{Unit} or C{str}).
313 @raise TypeError: Invalid B{C{units}}.
314 '''
315 self._units = _unitsBase._xUnits(units, Base=Float)
317 @property_RO
318 def wrap(self):
319 '''Get the C{wrap} setting (C{bool} or C{None}).
320 '''
321 return _xkwds_get(self._kwds, wrap=None)
324class FrechetDegrees(Frechet):
325 '''DEPRECATED, use an other C{Frechet*} class.
326 '''
327 _units = _unitsBase._Str_degrees
329 if _FOR_DOCS:
330 __init__ = Frechet.__init__
331 discrete = Frechet.discrete
333 def distance(self, point1, point2, *args, **kwds): # PYCHOK no cover
334 '''I{Must be overloaded}.'''
335 self._notOverloaded(point1, point2, *args, **kwds)
338class FrechetRadians(Frechet):
339 '''DEPRECATED, use an other C{Frechet*} class.
340 '''
341 _units = _unitsBase._Str_radians
343 if _FOR_DOCS:
344 __init__ = Frechet.__init__
345 discrete = Frechet.discrete
347 def distance(self, point1, point2, *args, **kwds): # PYCHOK no cover
348 '''I{Must be overloaded}.'''
349 self._notOverloaded(point1, point2, *args, **kwds)
351 def point(self, point):
352 '''Return B{C{point}} as L{PhiLam2Tuple} to maintain
353 I{backward compatibility} of L{FrechetRadians}.
355 @return: A L{PhiLam2Tuple}C{(phi, lam)}.
356 '''
357 try:
358 return point.philam
359 except AttributeError:
360 return PhiLam2Tuple(radians(point.lat), radians(point.lon))
363class _FrechetMeterRadians(Frechet):
364 '''(INTERNAL) Returning C{meter} or C{radians} depending on
365 the optional keyword arguments supplied at instantiation
366 of the C{Frechet*} sub-class.
367 '''
368 _units = _unitsBase._Str_meter
369 _units_ = _unitsBase._Str_radians
371 def discrete(self, point2s, fraction=None):
372 '''Overloaded method L{Frechet.discrete} to determine
373 the distance function and units from the optional
374 keyword arguments given at this instantiation, see
375 property C{kwds}.
377 @see: L{Frechet.discrete} for other details.
378 '''
379 return self._discrete(point2s, fraction, _formy._radistance(self))
381 @Property
382 def _func_(self): # see _formy._radistance
383 '''(INTERNAL) I{Must be overloaded}.'''
384 self._notOverloaded(**self.kwds)
386 @_func_.setter_ # PYCHOK setter_underscore!
387 def _func_(self, func):
388 return _formy._Propy(func, 3, self.kwds)
391class FrechetCosineLaw(_FrechetMeterRadians):
392 '''Compute the C{Frechet} distance with functionn L{pygeodesy.cosineLaw}.
394 @note: See note at function L{pygeodesy.vincentys_}.
395 '''
396 def __init__(self, point1s, **fraction_name__corr_earth_wrap):
397 '''New L{FrechetCosineLaw} calculator/interpolator.
399 @kwarg fraction_name__corr_earth_wrap: Optional
400 C{B{fraction}=None} and C{B{name}=NN} and keyword
401 arguments for function L{pygeodesy.cosineLaw}.
403 @see: L{Frechet.__init__} for details about B{C{point1s}},
404 B{C{fraction}}, B{C{name}} and other exceptions.
405 '''
406 Frechet.__init__(self, point1s, **fraction_name__corr_earth_wrap)
407 self._func = _formy.cosineLaw
408 self._func_ = _formy.cosineLaw_
410 if _FOR_DOCS:
411 discrete = Frechet.discrete
414class FrechetDistanceTo(Frechet): # FrechetMeter
415 '''Compute the C{Frechet} distance with the point1s' C{LatLon.distanceTo} method.
416 '''
417 _units = _unitsBase._Str_meter
419 def __init__(self, point1s, **fraction_name__distanceTo_kwds):
420 '''New L{FrechetDistanceTo} calculator/interpolator.
422 @kwarg fraction_name__distanceTo_kwds: Optional C{B{fraction}=None}
423 and C{B{name}=NN} and keyword arguments for
424 each B{C{point1s}}' C{LatLon.distanceTo} method.
426 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}},
427 B{C{name}} and other exceptions.
429 @note: All B{C{point1s}} I{must} be instances of the same ellipsoidal
430 or spherical C{LatLon} class.
431 '''
432 Frechet.__init__(self, point1s, **fraction_name__distanceTo_kwds)
434 if _FOR_DOCS:
435 discrete = Frechet.discrete
437 def distance(self, p1, p2):
438 '''Return the distance in C{meter}.
439 '''
440 return p1.distanceTo(p2, **self._kwds)
442 def _points2(self, points):
443 '''(INTERNAL) Check a set of points.
444 '''
445 np, ps = Frechet._points2(self, points)
446 return np, _distanceTo(FrechetError, points=ps)
449class FrechetEquirectangular(Frechet):
450 '''Compute the C{Frechet} distance with function L{pygeodesy.equirectangular}.
451 '''
452 _units = _unitsBase._Str_radians2
454 def __init__(self, point1s, **fraction_name__adjust_limit_wrap):
455 '''New L{FrechetEquirectangular} calculator/interpolator.
457 @kwarg fraction_name__adjust_limit_wrap: Optional C{B{fraction}=None}
458 and C{B{name}=NN} and keyword arguments for
459 function L{pygeodesy.equirectangular} I{with
460 default} C{B{limit}=0} for I{backward compatibility}.
462 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}},
463 B{C{name}} and other exceptions.
464 '''
465 Frechet.__init__(self, point1s, **_xkwds(fraction_name__adjust_limit_wrap,
466 limit=0))
467 self._func = _formy._equirectangular # helper
469 if _FOR_DOCS:
470 discrete = Frechet.discrete
473class FrechetEuclidean(_FrechetMeterRadians):
474 '''Compute the C{Frechet} distance with function L{pygeodesy.euclidean}.
475 '''
476 def __init__(self, point1s, **fraction_name__adjust_radius_wrap): # was=True
477 '''New L{FrechetEuclidean} calculator/interpolator.
479 @kwarg fraction_name__adjust_radius_wrap: Optional C{B{fraction}=None}
480 and C{B{name}=NN} and keyword arguments for
481 function L{pygeodesy.euclidean}.
483 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}},
484 B{C{name}} and other exceptions.
485 '''
486 Frechet.__init__(self, point1s, **fraction_name__adjust_radius_wrap)
487 self._func = _formy.euclidean
488 self._func_ = _formy.euclidean_
490 if _FOR_DOCS:
491 discrete = Frechet.discrete
494class FrechetExact(Frechet):
495 '''Compute the C{Frechet} distance with method L{GeodesicExact}C{.Inverse}.
496 '''
497 _units = _unitsBase._Str_degrees
499 def __init__(self, point1s, datum=None, **fraction_name__wrap):
500 '''New L{FrechetExact} calculator/interpolator.
502 @kwarg datum: Datum to override the default C{Datums.WGS84} and first
503 B{C{point1s}}' datum (L{Datum}, L{Ellipsoid}, L{Ellipsoid2}
504 or L{a_f2Tuple}).
505 @kwarg fraction_name__wrap: Optional C{B{fraction}=None} and C{B{name}=NN}
506 and keyword argument for method C{Inverse1} of class
507 L{geodesicx.GeodesicExact}.
509 @raise TypeError: Invalid B{C{datum}}.
511 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}},
512 B{C{name}} and other exceptions.
513 '''
514 Frechet.__init__(self, point1s, **fraction_name__wrap)
515 self._datum_setter(datum)
516 self._func = self.datum.ellipsoid.geodesicx.Inverse1 # note -x
518 if _FOR_DOCS:
519 discrete = Frechet.discrete
522class FrechetFlatLocal(_FrechetMeterRadians):
523 '''Compute the C{Frechet} distance with function L{pygeodesy.flatLocal_}/L{pygeodesy.hubeny}.
524 '''
525 _units_ = _unitsBase._Str_radians2 # see L{flatLocal_}
527 def __init__(self, point1s, **fraction_name__datum_scaled_wrap):
528 '''New L{FrechetFlatLocal}/L{FrechetHubeny} calculator/interpolator.
530 @kwarg fraction_name__datum_scaled_wrap: Optional C{B{fraction}=None}
531 and C{B{name}=NN} and keyword arguments for
532 function L{pygeodesy.flatLocal}.
534 @see: L{Frechet.__init__} for details about B{C{point1s}}, B{C{fraction}},
535 B{C{name}} and other exceptions.
537 @note: The distance C{units} are C{radians squared}, not C{radians}.
538 '''
539 Frechet.__init__(self, point1s, **fraction_name__datum_scaled_wrap)
540 self._func = _formy.flatLocal
541 self._func_ = self.datum.ellipsoid._hubeny_2
543 if _FOR_DOCS:
544 discrete = Frechet.discrete
547class FrechetFlatPolar(_FrechetMeterRadians):
548 '''Compute the C{Frechet} distance with function L{flatPolar_}.
549 '''
550 def __init__(self, point1s, **fraction_name__radius_wrap):
551 '''New L{FrechetFlatPolar} calculator/interpolator.
553 @kwarg fraction_name__radius_wrap: Optional C{B{fraction}=None}
554 and C{B{name}=NN} and keyword arguments
555 for function L{pygeodesy.flatPolar}.
557 @see: L{Frechet.__init__} for details about B{C{point1s}},
558 B{C{fraction}}, B{C{name}} and other exceptions.
559 '''
560 Frechet.__init__(self, point1s, **fraction_name__radius_wrap)
561 self._func = _formy.flatPolar
562 self._func_ = _formy.flatPolar_
564 if _FOR_DOCS:
565 discrete = Frechet.discrete
568class FrechetHaversine(_FrechetMeterRadians):
569 '''Compute the C{Frechet} distance with function L{pygeodesy.haversine_}.
571 @note: See note at function L{pygeodesy.vincentys_}.
572 '''
573 def __init__(self, point1s, **fraction_name__radius_wrap):
574 '''New L{FrechetHaversine} calculator/interpolator.
576 @kwarg fraction_name__radius_wrap: Optional C{B{fraction}=None}
577 and C{B{name}=NN} and keyword arguments
578 for function L{pygeodesy.haversine}.
580 @see: L{Frechet.__init__} for details about B{C{point1s}},
581 B{C{fraction}}, B{C{name}} and other exceptions.
582 '''
583 Frechet.__init__(self, point1s, **fraction_name__radius_wrap)
584 self._func = _formy.haversine
585 self._func_ = _formy.haversine_
587 if _FOR_DOCS:
588 discrete = Frechet.discrete
591class FrechetHubeny(FrechetFlatLocal): # for Karl Hubeny
592 if _FOR_DOCS:
593 __doc__ = FrechetFlatLocal.__doc__
594 __init__ = FrechetFlatLocal.__init__
595 discrete = FrechetFlatLocal.discrete
596 distance = FrechetFlatLocal.discrete
599class FrechetKarney(Frechet):
600 '''Compute the C{Frechet} distance with I{Karney}'s U{geographiclib
601 <https://PyPI.org/project/geographiclib>} U{geodesic.Geodesic
602 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}C{.Inverse}
603 method.
604 '''
605 _units = _unitsBase._Str_degrees
607 def __init__(self, point1s, datum=None, **fraction_name__wrap):
608 '''New L{FrechetKarney} calculator/interpolator.
610 @kwarg datum: Datum to override the default C{Datums.WGS84} and
611 first B{C{knots}}' datum (L{Datum}, L{Ellipsoid},
612 L{Ellipsoid2} or L{a_f2Tuple}).
613 @kwarg fraction_name__wrap: Optional C{B{fraction}=None} and
614 C{B{name}=NN} and keyword arguments for
615 method C{Inverse1} of class L{geodesicw.Geodesic}.
617 @raise ImportError: Package U{geographiclib
618 <https://PyPI.org/project/geographiclib>} missing.
620 @raise TypeError: Invalid B{C{datum}}.
622 @see: L{Frechet.__init__} for details about B{C{point1s}},
623 B{C{fraction}}, B{C{name}} and other exceptions.
624 '''
625 Frechet.__init__(self, point1s, **fraction_name__wrap)
626 self._datum_setter(datum)
627 self._func = self.datum.ellipsoid.geodesic.Inverse1
629 if _FOR_DOCS:
630 discrete = Frechet.discrete
633class FrechetThomas(_FrechetMeterRadians):
634 '''Compute the C{Frechet} distance with function L{pygeodesy.thomas_}.
635 '''
636 def __init__(self, point1s, **fraction_name__datum_wrap):
637 '''New L{FrechetThomas} calculator/interpolator.
639 @kwarg fraction_name__datum_wrap: Optional C{B{fraction}=None}
640 and C{B{name}=NN} and keyword arguments
641 for function L{pygeodesy.thomas}.
643 @see: L{Frechet.__init__} for details about B{C{point1s}},
644 B{C{fraction}}, B{C{name}} and other exceptions.
645 '''
646 Frechet.__init__(self, point1s, **fraction_name__datum_wrap)
647 self._func = _formy.thomas
648 self._func_ = _formy.thomas_
650 if _FOR_DOCS:
651 discrete = Frechet.discrete
654class FrechetVincentys(_FrechetMeterRadians):
655 '''Compute the C{Frechet} distance with function L{pygeodesy.vincentys_}.
657 @note: See note at function L{pygeodesy.vincentys_}.
658 '''
659 def __init__(self, point1s, **fraction_name__radius_wrap):
660 '''New L{FrechetVincentys} calculator/interpolator.
662 @kwarg fraction_name__radius_wrap: Optional C{B{fraction}=None}
663 and C{B{name}=NN} and keyword arguments
664 for function L{pygeodesy.vincentys}.
666 @see: L{Frechet.__init__} for details about B{C{point1s}},
667 B{C{fraction}}, B{C{name}} and other exceptions.
668 '''
669 Frechet.__init__(self, point1s, **fraction_name__radius_wrap)
670 self._func = _formy.vincentys
671 self._func_ = _formy.vincentys_
673 if _FOR_DOCS:
674 discrete = Frechet.discrete
677def _frechet_(ni, fi, nj, fj, dF, units): # MCCABE 14
678 '''(INTERNAL) Recursive core of function L{frechet_}
679 and method C{discrete} of C{Frechet...} classes.
680 '''
681 iFs = {}
683 def iF(i): # cache index, depth ints and floats
684 return iFs.setdefault(i, i)
686 cF = _defaultdict(dict)
688 def _rF(i, j, r): # recursive Fréchet
689 i = iF(i)
690 j = iF(j)
691 try:
692 t = cF[i][j]
693 except KeyError:
694 r = iF(r + 1)
695 try:
696 if i > 0:
697 if j > 0:
698 t = min(_rF(i - fi, j, r),
699 _rF(i - fi, j - fj, r),
700 _rF(i, j - fj, r))
701 elif j < 0:
702 raise IndexError
703 else: # j == 0
704 t = _rF(i - fi, 0, r)
705 elif i < 0:
706 raise IndexError
708 elif j > 0: # i == 0
709 t = _rF(0, j - fj, r)
710 elif j < 0: # i == 0
711 raise IndexError
712 else: # i == j == 0
713 t = (NINF, i, j, r)
715 d = dF(i, j)
716 if d > t[0]:
717 t = (d, i, j, r)
718 except IndexError:
719 t = (INF, i, j, r)
720 cF[i][j] = t
721 return t
723 t = _rF(ni - 1, nj - 1, 0)
724 t += (sum(map(len, cF.values())), units)
725# del cF, iFs
726 return Frechet6Tuple(t) # *t
729def frechet_(point1s, point2s, distance=None, units=NN):
730 '''Compute the I{discrete} U{Fréchet<https://WikiPedia.org/wiki/Frechet_distance>}
731 distance between two paths, each given as a set of points.
733 @arg point1s: First set of points (C{LatLon}[], L{Numpy2LatLon}[],
734 L{Tuple2LatLon}[] or C{other}[]).
735 @arg point2s: Second set of points (C{LatLon}[], L{Numpy2LatLon}[],
736 L{Tuple2LatLon}[] or C{other}[]).
737 @kwarg distance: Callable returning the distance between a B{C{point1s}}
738 and a B{C{point2s}} point (signature C{(point1, point2)}).
739 @kwarg units: Optional, the distance units (C{Unit} or C{str}).
741 @return: A L{Frechet6Tuple}C{(fd, fi1, fi2, r, n, units)} where C{fi1}
742 and C{fi2} are type C{int} indices into B{C{point1s}} respectively
743 B{C{point2s}}.
745 @raise FrechetError: Insufficient number of B{C{point1s}} or B{C{point2s}}.
747 @raise RecursionError: Recursion depth exceeded, see U{sys.getrecursionlimit()
748 <https://docs.Python.org/3/library/sys.html#sys.getrecursionlimit>}.
750 @raise TypeError: If B{C{distance}} is not a callable.
752 @note: Function L{frechet_} does I{not} support I{fractional} indices
753 for intermediate B{C{point1s}} and B{C{point2s}}.
754 '''
755 _xcallable(distance=distance)
757 n1, ps1 = _points2(point1s, closed=False, Error=FrechetError)
758 n2, ps2 = _points2(point2s, closed=False, Error=FrechetError)
760 def _dF(i1, i2):
761 return distance(ps1[i1], ps2[i2])
763 return _frechet_(n1, 1, n2, 1, _dF, units)
766class Frechet6Tuple(_NamedTuple):
767 '''6-Tuple C{(fd, fi1, fi2, r, n, units)} with the I{discrete}
768 U{Fréchet<https://WikiPedia.org/wiki/Frechet_distance>} distance
769 C{fd}, I{fractional} indices C{fi1} and C{fi2} as C{FIx}, the
770 recursion depth C{r}, the number of distances computed C{n} and
771 the L{units} class or class or name of the distance C{units}.
773 If I{fractional} indices C{fi1} and C{fi2} are C{int}, the
774 returned C{fd} is the distance between C{point1s[fi1]} and
775 C{point2s[fi2]}. For C{float} indices, the distance is
776 between an intermediate point along C{point1s[int(fi1)]} and
777 C{point1s[int(fi1) + 1]} respectively an intermediate point
778 along C{point2s[int(fi2)]} and C{point2s[int(fi2) + 1]}.
780 Use function L{fractional} to compute the point at a
781 I{fractional} index.
782 '''
783 _Names_ = ('fd', 'fi1', 'fi2', 'r', _n_, _units_)
784 _Units_ = (_Pass, FIx, FIx, Number_, Number_, _Pass)
786 def toUnits(self, **Error_name): # PYCHOK expected
787 '''Overloaded C{_NamedTuple.toUnits} for C{fd} units.
788 '''
789 U = _unitsBase._xUnit(self.units, Float) # PYCHOK expected
790 return _NamedTuple.toUnits(self.reUnit(U), **Error_name) # PYCHOK self
792# def __gt__(self, other):
793# _xinstanceof(Frechet6Tuple, other=other)
794# return self if self.fd > other.fd else other # PYCHOK .fd=[0]
795#
796# def __lt__(self, other):
797# _xinstanceof(Frechet6Tuple, other=other)
798# return self if self.fd < other.fd else other # PYCHOK .fd=[0]
800# **) MIT License
801#
802# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
803#
804# Permission is hereby granted, free of charge, to any person obtaining a
805# copy of this software and associated documentation files (the "Software"),
806# to deal in the Software without restriction, including without limitation
807# the rights to use, copy, modify, merge, publish, distribute, sublicense,
808# and/or sell copies of the Software, and to permit persons to whom the
809# Software is furnished to do so, subject to the following conditions:
810#
811# The above copyright notice and this permission notice shall be included
812# in all copies or substantial portions of the Software.
813#
814# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
815# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
816# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
817# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
818# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
819# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
820# OTHER DEALINGS IN THE SOFTWARE.