Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

base.py 28KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926
  1. # -*- test-case-name: twisted.positioning.test.test_base,twisted.positioning.test.test_sentence -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Generic positioning base classes.
  6. @since: 14.0
  7. """
  8. from functools import partial
  9. from operator import attrgetter
  10. from typing import ClassVar, Sequence
  11. from zope.interface import implementer
  12. from constantly import NamedConstant, Names # type: ignore[import]
  13. from twisted.positioning import ipositioning
  14. from twisted.python.util import FancyEqMixin
  15. MPS_PER_KNOT = 0.5144444444444444
  16. MPS_PER_KPH = 0.27777777777777777
  17. METERS_PER_FOOT = 0.3048
  18. class Angles(Names):
  19. """
  20. The types of angles.
  21. @cvar LATITUDE: Angle representing a latitude of an object.
  22. @type LATITUDE: L{NamedConstant}
  23. @cvar LONGITUDE: Angle representing the longitude of an object.
  24. @type LONGITUDE: L{NamedConstant}
  25. @cvar HEADING: Angle representing the heading of an object.
  26. @type HEADING: L{NamedConstant}
  27. @cvar VARIATION: Angle representing a magnetic variation.
  28. @type VARIATION: L{NamedConstant}
  29. """
  30. LATITUDE = NamedConstant()
  31. LONGITUDE = NamedConstant()
  32. HEADING = NamedConstant()
  33. VARIATION = NamedConstant()
  34. class Directions(Names):
  35. """
  36. The four cardinal directions (north, east, south, west).
  37. """
  38. NORTH = NamedConstant()
  39. EAST = NamedConstant()
  40. SOUTH = NamedConstant()
  41. WEST = NamedConstant()
  42. @implementer(ipositioning.IPositioningReceiver)
  43. class BasePositioningReceiver:
  44. """
  45. A base positioning receiver.
  46. This class would be a good base class for building positioning
  47. receivers. It implements the interface (so you don't have to) with stub
  48. methods.
  49. People who want to implement positioning receivers should subclass this
  50. class and override the specific callbacks they want to handle.
  51. """
  52. def timeReceived(self, time):
  53. """
  54. Implements L{IPositioningReceiver.timeReceived} stub.
  55. """
  56. def headingReceived(self, heading):
  57. """
  58. Implements L{IPositioningReceiver.headingReceived} stub.
  59. """
  60. def speedReceived(self, speed):
  61. """
  62. Implements L{IPositioningReceiver.speedReceived} stub.
  63. """
  64. def climbReceived(self, climb):
  65. """
  66. Implements L{IPositioningReceiver.climbReceived} stub.
  67. """
  68. def positionReceived(self, latitude, longitude):
  69. """
  70. Implements L{IPositioningReceiver.positionReceived} stub.
  71. """
  72. def positionErrorReceived(self, positionError):
  73. """
  74. Implements L{IPositioningReceiver.positionErrorReceived} stub.
  75. """
  76. def altitudeReceived(self, altitude):
  77. """
  78. Implements L{IPositioningReceiver.altitudeReceived} stub.
  79. """
  80. def beaconInformationReceived(self, beaconInformation):
  81. """
  82. Implements L{IPositioningReceiver.beaconInformationReceived} stub.
  83. """
  84. class InvalidSentence(Exception):
  85. """
  86. An exception raised when a sentence is invalid.
  87. """
  88. class InvalidChecksum(Exception):
  89. """
  90. An exception raised when the checksum of a sentence is invalid.
  91. """
  92. class Angle(FancyEqMixin):
  93. """
  94. An object representing an angle.
  95. @cvar _RANGE_EXPRESSIONS: A collection of expressions for the allowable
  96. range for the angular value of a particular coordinate value.
  97. @type _RANGE_EXPRESSIONS: C{dict} of L{Angles} constants to callables
  98. @cvar _ANGLE_TYPE_NAMES: English names for angle types.
  99. @type _ANGLE_TYPE_NAMES: C{dict} of L{Angles} constants to C{str}
  100. """
  101. _RANGE_EXPRESSIONS = {
  102. Angles.LATITUDE: lambda latitude: -90.0 < latitude < 90.0,
  103. Angles.LONGITUDE: lambda longitude: -180.0 < longitude < 180.0,
  104. Angles.HEADING: lambda heading: 0 <= heading < 360,
  105. Angles.VARIATION: lambda variation: -180 < variation <= 180,
  106. }
  107. _ANGLE_TYPE_NAMES = {
  108. Angles.LATITUDE: "Latitude",
  109. Angles.LONGITUDE: "Longitude",
  110. Angles.VARIATION: "Variation",
  111. Angles.HEADING: "Heading",
  112. }
  113. compareAttributes: ClassVar[Sequence[str]] = (
  114. "angleType",
  115. "inDecimalDegrees",
  116. )
  117. def __init__(self, angle=None, angleType=None):
  118. """
  119. Initializes an angle.
  120. @param angle: The value of the angle in decimal degrees. (L{None} if
  121. unknown).
  122. @type angle: C{float} or L{None}
  123. @param angleType: A symbolic constant describing the angle type. Should
  124. be one of L{Angles} or {None} if unknown.
  125. @raises ValueError: If the angle type is not the default argument,
  126. but it is an unknown type (not in C{Angle._RANGE_EXPRESSIONS}),
  127. or it is a known type but the supplied value was out of the
  128. allowable range for said type.
  129. """
  130. if angleType is not None and angleType not in self._RANGE_EXPRESSIONS:
  131. raise ValueError("Unknown angle type")
  132. if angle is not None and angleType is not None:
  133. rangeExpression = self._RANGE_EXPRESSIONS[angleType]
  134. if not rangeExpression(angle):
  135. template = "Angle {0} not in allowed range for type {1}"
  136. raise ValueError(template.format(angle, angleType))
  137. self.angleType = angleType
  138. self._angle = angle
  139. @property
  140. def inDecimalDegrees(self):
  141. """
  142. The value of this angle in decimal degrees. This value is immutable.
  143. @return: This angle expressed in decimal degrees, or L{None} if the
  144. angle is unknown.
  145. @rtype: C{float} (or L{None})
  146. """
  147. return self._angle
  148. @property
  149. def inDegreesMinutesSeconds(self):
  150. """
  151. The value of this angle as a degrees, minutes, seconds tuple. This
  152. value is immutable.
  153. @return: This angle expressed in degrees, minutes, seconds. L{None} if
  154. the angle is unknown.
  155. @rtype: 3-C{tuple} of C{int} (or L{None})
  156. """
  157. if self._angle is None:
  158. return None
  159. degrees = abs(int(self._angle))
  160. fractionalDegrees = abs(self._angle - int(self._angle))
  161. decimalMinutes = 60 * fractionalDegrees
  162. minutes = int(decimalMinutes)
  163. fractionalMinutes = decimalMinutes - int(decimalMinutes)
  164. decimalSeconds = 60 * fractionalMinutes
  165. return degrees, minutes, int(decimalSeconds)
  166. def setSign(self, sign):
  167. """
  168. Sets the sign of this angle.
  169. @param sign: The new sign. C{1} for positive and C{-1} for negative
  170. signs, respectively.
  171. @type sign: C{int}
  172. @raise ValueError: If the C{sign} parameter is not C{-1} or C{1}.
  173. """
  174. if sign not in (-1, 1):
  175. raise ValueError("bad sign (got %s, expected -1 or 1)" % sign)
  176. self._angle = sign * abs(self._angle)
  177. def __float__(self):
  178. """
  179. Returns this angle as a float.
  180. @return: The float value of this angle, expressed in degrees.
  181. @rtype: C{float}
  182. """
  183. return self._angle
  184. def __repr__(self) -> str:
  185. """
  186. Returns a string representation of this angle.
  187. @return: The string representation.
  188. @rtype: C{str}
  189. """
  190. return "<{s._angleTypeNameRepr} ({s._angleValueRepr})>".format(s=self)
  191. @property
  192. def _angleValueRepr(self):
  193. """
  194. Returns a string representation of the angular value of this angle.
  195. This is a helper function for the actual C{__repr__}.
  196. @return: The string representation.
  197. @rtype: C{str}
  198. """
  199. if self.inDecimalDegrees is not None:
  200. return "%s degrees" % round(self.inDecimalDegrees, 2)
  201. else:
  202. return "unknown value"
  203. @property
  204. def _angleTypeNameRepr(self):
  205. """
  206. Returns a string representation of the type of this angle.
  207. This is a helper function for the actual C{__repr__}.
  208. @return: The string representation.
  209. @rtype: C{str}
  210. """
  211. try:
  212. return self._ANGLE_TYPE_NAMES[self.angleType]
  213. except KeyError:
  214. return "Angle of unknown type"
  215. class Heading(Angle):
  216. """
  217. The heading of a mobile object.
  218. @ivar variation: The (optional) magnetic variation.
  219. The sign of the variation is positive for variations towards the east
  220. (clockwise from north), and negative for variations towards the west
  221. (counterclockwise from north).
  222. If the variation is unknown or not applicable, this is L{None}.
  223. @type variation: C{Angle} or L{None}.
  224. @ivar correctedHeading: The heading, corrected for variation. If the
  225. variation is unknown (L{None}), is None. This attribute is read-only
  226. (its value is determined by the angle and variation attributes). The
  227. value is coerced to being between 0 (inclusive) and 360 (exclusive).
  228. """
  229. def __init__(self, angle=None, variation=None):
  230. """
  231. Initializes an angle with an optional variation.
  232. """
  233. Angle.__init__(self, angle, Angles.HEADING)
  234. self.variation = variation
  235. @classmethod
  236. def fromFloats(cls, angleValue=None, variationValue=None):
  237. """
  238. Constructs a Heading from the float values of the angle and variation.
  239. @param angleValue: The angle value of this heading.
  240. @type angleValue: C{float}
  241. @param variationValue: The value of the variation of this heading.
  242. @type variationValue: C{float}
  243. @return: A L{Heading} with the given values.
  244. """
  245. variation = Angle(variationValue, Angles.VARIATION)
  246. return cls(angleValue, variation)
  247. @property
  248. def correctedHeading(self):
  249. """
  250. Corrects the heading by the given variation. This is sometimes known as
  251. the true heading.
  252. @return: The heading, corrected by the variation. If the variation or
  253. the angle are unknown, returns L{None}.
  254. @rtype: C{float} or L{None}
  255. """
  256. if self._angle is None or self.variation is None:
  257. return None
  258. angle = (self.inDecimalDegrees - self.variation.inDecimalDegrees) % 360
  259. return Angle(angle, Angles.HEADING)
  260. def setSign(self, sign):
  261. """
  262. Sets the sign of the variation of this heading.
  263. @param sign: The new sign. C{1} for positive and C{-1} for negative
  264. signs, respectively.
  265. @type sign: C{int}
  266. @raise ValueError: If the C{sign} parameter is not C{-1} or C{1}.
  267. """
  268. if self.variation.inDecimalDegrees is None:
  269. raise ValueError("can't set the sign of an unknown variation")
  270. self.variation.setSign(sign)
  271. compareAttributes = list(Angle.compareAttributes) + ["variation"]
  272. def __repr__(self) -> str:
  273. """
  274. Returns a string representation of this angle.
  275. @return: The string representation.
  276. @rtype: C{str}
  277. """
  278. if self.variation is None:
  279. variationRepr = "unknown variation"
  280. else:
  281. variationRepr = repr(self.variation)
  282. return "<{} ({}, {})>".format(
  283. self._angleTypeNameRepr,
  284. self._angleValueRepr,
  285. variationRepr,
  286. )
  287. class Coordinate(Angle):
  288. """
  289. A coordinate.
  290. @ivar angle: The value of the coordinate in decimal degrees, with the usual
  291. rules for sign (northern and eastern hemispheres are positive, southern
  292. and western hemispheres are negative).
  293. @type angle: C{float}
  294. """
  295. def __init__(self, angle, coordinateType=None):
  296. """
  297. Initializes a coordinate.
  298. @param angle: The angle of this coordinate in decimal degrees. The
  299. hemisphere is determined by the sign (north and east are positive).
  300. If this coordinate describes a latitude, this value must be within
  301. -90.0 and +90.0 (exclusive). If this value describes a longitude,
  302. this value must be within -180.0 and +180.0 (exclusive).
  303. @type angle: C{float}
  304. @param coordinateType: The coordinate type. One of L{Angles.LATITUDE},
  305. L{Angles.LONGITUDE} or L{None} if unknown.
  306. """
  307. if coordinateType not in [Angles.LATITUDE, Angles.LONGITUDE, None]:
  308. raise ValueError(
  309. "coordinateType must be one of Angles.LATITUDE, "
  310. "Angles.LONGITUDE or None, was {!r}".format(coordinateType)
  311. )
  312. Angle.__init__(self, angle, coordinateType)
  313. @property
  314. def hemisphere(self):
  315. """
  316. Gets the hemisphere of this coordinate.
  317. @return: A symbolic constant representing a hemisphere (one of
  318. L{Angles})
  319. """
  320. if self.angleType is Angles.LATITUDE:
  321. if self.inDecimalDegrees < 0:
  322. return Directions.SOUTH
  323. else:
  324. return Directions.NORTH
  325. elif self.angleType is Angles.LONGITUDE:
  326. if self.inDecimalDegrees < 0:
  327. return Directions.WEST
  328. else:
  329. return Directions.EAST
  330. else:
  331. raise ValueError("unknown coordinate type (cant find hemisphere)")
  332. class Altitude(FancyEqMixin):
  333. """
  334. An altitude.
  335. @ivar inMeters: The altitude represented by this object, in meters. This
  336. attribute is read-only.
  337. @type inMeters: C{float}
  338. @ivar inFeet: As above, but expressed in feet.
  339. @type inFeet: C{float}
  340. """
  341. compareAttributes = ("inMeters",)
  342. def __init__(self, altitude):
  343. """
  344. Initializes an altitude.
  345. @param altitude: The altitude in meters.
  346. @type altitude: C{float}
  347. """
  348. self._altitude = altitude
  349. @property
  350. def inFeet(self):
  351. """
  352. Gets the altitude this object represents, in feet.
  353. @return: The altitude, expressed in feet.
  354. @rtype: C{float}
  355. """
  356. return self._altitude / METERS_PER_FOOT
  357. @property
  358. def inMeters(self):
  359. """
  360. Returns the altitude this object represents, in meters.
  361. @return: The altitude, expressed in feet.
  362. @rtype: C{float}
  363. """
  364. return self._altitude
  365. def __float__(self):
  366. """
  367. Returns the altitude represented by this object expressed in meters.
  368. @return: The altitude represented by this object, expressed in meters.
  369. @rtype: C{float}
  370. """
  371. return self._altitude
  372. def __repr__(self) -> str:
  373. """
  374. Returns a string representation of this altitude.
  375. @return: The string representation.
  376. @rtype: C{str}
  377. """
  378. return f"<Altitude ({self._altitude} m)>"
  379. class _BaseSpeed(FancyEqMixin):
  380. """
  381. An object representing the abstract concept of the speed (rate of
  382. movement) of a mobile object.
  383. This primarily has behavior for converting between units and comparison.
  384. """
  385. compareAttributes = ("inMetersPerSecond",)
  386. def __init__(self, speed):
  387. """
  388. Initializes a speed.
  389. @param speed: The speed that this object represents, expressed in
  390. meters per second.
  391. @type speed: C{float}
  392. @raises ValueError: Raised if value was invalid for this particular
  393. kind of speed. Only happens in subclasses.
  394. """
  395. self._speed = speed
  396. @property
  397. def inMetersPerSecond(self):
  398. """
  399. The speed that this object represents, expressed in meters per second.
  400. This attribute is immutable.
  401. @return: The speed this object represents, in meters per second.
  402. @rtype: C{float}
  403. """
  404. return self._speed
  405. @property
  406. def inKnots(self):
  407. """
  408. Returns the speed represented by this object, expressed in knots. This
  409. attribute is immutable.
  410. @return: The speed this object represents, in knots.
  411. @rtype: C{float}
  412. """
  413. return self._speed / MPS_PER_KNOT
  414. def __float__(self):
  415. """
  416. Returns the speed represented by this object expressed in meters per
  417. second.
  418. @return: The speed represented by this object, expressed in meters per
  419. second.
  420. @rtype: C{float}
  421. """
  422. return self._speed
  423. def __repr__(self) -> str:
  424. """
  425. Returns a string representation of this speed object.
  426. @return: The string representation.
  427. @rtype: C{str}
  428. """
  429. speedValue = round(self.inMetersPerSecond, 2)
  430. return f"<{self.__class__.__name__} ({speedValue} m/s)>"
  431. class Speed(_BaseSpeed):
  432. """
  433. The speed (rate of movement) of a mobile object.
  434. """
  435. def __init__(self, speed):
  436. """
  437. Initializes a L{Speed} object.
  438. @param speed: The speed that this object represents, expressed in
  439. meters per second.
  440. @type speed: C{float}
  441. @raises ValueError: Raised if C{speed} is negative.
  442. """
  443. if speed < 0:
  444. raise ValueError(f"negative speed: {speed!r}")
  445. _BaseSpeed.__init__(self, speed)
  446. class Climb(_BaseSpeed):
  447. """
  448. The climb ("vertical speed") of an object.
  449. """
  450. def __init__(self, climb):
  451. """
  452. Initializes a L{Climb} object.
  453. @param climb: The climb that this object represents, expressed in
  454. meters per second.
  455. @type climb: C{float}
  456. """
  457. _BaseSpeed.__init__(self, climb)
  458. class PositionError(FancyEqMixin):
  459. """
  460. Position error information.
  461. @cvar _ALLOWABLE_THRESHOLD: The maximum allowable difference between PDOP
  462. and the geometric mean of VDOP and HDOP. That difference is supposed
  463. to be zero, but can be non-zero because of rounding error and limited
  464. reporting precision. You should never have to change this value.
  465. @type _ALLOWABLE_THRESHOLD: C{float}
  466. @cvar _DOP_EXPRESSIONS: A mapping of DOP types (C[hvp]dop) to a list of
  467. callables that take self and return that DOP type, or raise
  468. C{TypeError}. This allows a DOP value to either be returned directly
  469. if it's know, or computed from other DOP types if it isn't.
  470. @type _DOP_EXPRESSIONS: C{dict} of C{str} to callables
  471. @ivar pdop: The position dilution of precision. L{None} if unknown.
  472. @type pdop: C{float} or L{None}
  473. @ivar hdop: The horizontal dilution of precision. L{None} if unknown.
  474. @type hdop: C{float} or L{None}
  475. @ivar vdop: The vertical dilution of precision. L{None} if unknown.
  476. @type vdop: C{float} or L{None}
  477. """
  478. compareAttributes = "pdop", "hdop", "vdop"
  479. def __init__(self, pdop=None, hdop=None, vdop=None, testInvariant=False):
  480. """
  481. Initializes a positioning error object.
  482. @param pdop: The position dilution of precision. L{None} if unknown.
  483. @type pdop: C{float} or L{None}
  484. @param hdop: The horizontal dilution of precision. L{None} if unknown.
  485. @type hdop: C{float} or L{None}
  486. @param vdop: The vertical dilution of precision. L{None} if unknown.
  487. @type vdop: C{float} or L{None}
  488. @param testInvariant: Flag to test if the DOP invariant is valid or
  489. not. If C{True}, the invariant (PDOP = (HDOP**2 + VDOP**2)*.5) is
  490. checked at every mutation. By default, this is false, because the
  491. vast majority of DOP-providing devices ignore this invariant.
  492. @type testInvariant: c{bool}
  493. """
  494. self._pdop = pdop
  495. self._hdop = hdop
  496. self._vdop = vdop
  497. self._testInvariant = testInvariant
  498. self._testDilutionOfPositionInvariant()
  499. _ALLOWABLE_TRESHOLD = 0.01
  500. def _testDilutionOfPositionInvariant(self):
  501. """
  502. Tests if this positioning error object satisfies the dilution of
  503. position invariant (PDOP = (HDOP**2 + VDOP**2)*.5), unless the
  504. C{self._testInvariant} instance variable is C{False}.
  505. @return: L{None} if the invariant was not satisfied or not tested.
  506. @raises ValueError: Raised if the invariant was tested but not
  507. satisfied.
  508. """
  509. if not self._testInvariant:
  510. return
  511. for x in (self.pdop, self.hdop, self.vdop):
  512. if x is None:
  513. return
  514. delta = abs(self.pdop - (self.hdop ** 2 + self.vdop ** 2) ** 0.5)
  515. if delta > self._ALLOWABLE_TRESHOLD:
  516. raise ValueError(
  517. "invalid combination of dilutions of precision: "
  518. "position: %s, horizontal: %s, vertical: %s"
  519. % (self.pdop, self.hdop, self.vdop)
  520. )
  521. _DOP_EXPRESSIONS = {
  522. "pdop": [
  523. lambda self: float(self._pdop),
  524. lambda self: (self._hdop ** 2 + self._vdop ** 2) ** 0.5,
  525. ],
  526. "hdop": [
  527. lambda self: float(self._hdop),
  528. lambda self: (self._pdop ** 2 - self._vdop ** 2) ** 0.5,
  529. ],
  530. "vdop": [
  531. lambda self: float(self._vdop),
  532. lambda self: (self._pdop ** 2 - self._hdop ** 2) ** 0.5,
  533. ],
  534. }
  535. def _getDOP(self, dopType):
  536. """
  537. Gets a particular dilution of position value.
  538. @param dopType: The type of dilution of position to get. One of
  539. ('pdop', 'hdop', 'vdop').
  540. @type dopType: C{str}
  541. @return: The DOP if it is known, L{None} otherwise.
  542. @rtype: C{float} or L{None}
  543. """
  544. for dopExpression in self._DOP_EXPRESSIONS[dopType]:
  545. try:
  546. return dopExpression(self)
  547. except TypeError:
  548. continue
  549. def _setDOP(self, dopType, value):
  550. """
  551. Sets a particular dilution of position value.
  552. @param dopType: The type of dilution of position to set. One of
  553. ('pdop', 'hdop', 'vdop').
  554. @type dopType: C{str}
  555. @param value: The value to set the dilution of position type to.
  556. @type value: C{float}
  557. If this position error tests dilution of precision invariants,
  558. it will be checked. If the invariant is not satisfied, the
  559. assignment will be undone and C{ValueError} is raised.
  560. """
  561. attributeName = "_" + dopType
  562. oldValue = getattr(self, attributeName)
  563. setattr(self, attributeName, float(value))
  564. try:
  565. self._testDilutionOfPositionInvariant()
  566. except ValueError:
  567. setattr(self, attributeName, oldValue)
  568. raise
  569. @property
  570. def pdop(self):
  571. return self._getDOP("pdop")
  572. @pdop.setter
  573. def pdop(self, value):
  574. return self._setDOP("pdop", value)
  575. @property
  576. def hdop(self):
  577. return self._getDOP("hdop")
  578. @hdop.setter
  579. def hdop(self, value):
  580. return self._setDOP("hdop", value)
  581. @property
  582. def vdop(self):
  583. return self._getDOP("vdop")
  584. @vdop.setter
  585. def vdop(self, value):
  586. return self._setDOP("vdop", value)
  587. _REPR_TEMPLATE = "<PositionError (pdop: %s, hdop: %s, vdop: %s)>"
  588. def __repr__(self) -> str:
  589. """
  590. Returns a string representation of positioning information object.
  591. @return: The string representation.
  592. @rtype: C{str}
  593. """
  594. return self._REPR_TEMPLATE % (self.pdop, self.hdop, self.vdop)
  595. class BeaconInformation:
  596. """
  597. Information about positioning beacons (a generalized term for the reference
  598. objects that help you determine your position, such as satellites or cell
  599. towers).
  600. @ivar seenBeacons: A set of visible beacons. Note that visible beacons are not
  601. necessarily used in acquiring a positioning fix.
  602. @type seenBeacons: C{set} of L{IPositioningBeacon}
  603. @ivar usedBeacons: A set of the beacons that were used in obtaining a
  604. positioning fix. This only contains beacons that are actually used, not
  605. beacons for which it is unknown if they are used or not.
  606. @type usedBeacons: C{set} of L{IPositioningBeacon}
  607. """
  608. def __init__(self, seenBeacons=()):
  609. """
  610. Initializes a beacon information object.
  611. @param seenBeacons: A collection of beacons that are currently seen.
  612. @type seenBeacons: iterable of L{IPositioningBeacon}s
  613. """
  614. self.seenBeacons = set(seenBeacons)
  615. self.usedBeacons = set()
  616. def __repr__(self) -> str:
  617. """
  618. Returns a string representation of this beacon information object.
  619. The beacons are sorted by their identifier.
  620. @return: The string representation.
  621. @rtype: C{str}
  622. """
  623. sortedBeacons = partial(sorted, key=attrgetter("identifier"))
  624. usedBeacons = sortedBeacons(self.usedBeacons)
  625. unusedBeacons = sortedBeacons(self.seenBeacons - self.usedBeacons)
  626. template = (
  627. "<BeaconInformation ("
  628. "used beacons ({numUsed}): {usedBeacons}, "
  629. "unused beacons: {unusedBeacons})>"
  630. )
  631. formatted = template.format(
  632. numUsed=len(self.usedBeacons),
  633. usedBeacons=usedBeacons,
  634. unusedBeacons=unusedBeacons,
  635. )
  636. return formatted
  637. @implementer(ipositioning.IPositioningBeacon)
  638. class PositioningBeacon:
  639. """
  640. A positioning beacon.
  641. @ivar identifier: The unique identifier for this beacon. This is usually
  642. an integer. For GPS, this is also known as the PRN.
  643. @type identifier: Pretty much anything that can be used as a unique
  644. identifier. Depends on the implementation.
  645. """
  646. def __init__(self, identifier):
  647. """
  648. Initializes a positioning beacon.
  649. @param identifier: The identifier for this beacon.
  650. @type identifier: Can be pretty much anything (see ivar documentation).
  651. """
  652. self.identifier = identifier
  653. def __hash__(self):
  654. """
  655. Returns the hash of the identifier for this beacon.
  656. @return: The hash of the identifier. (C{hash(self.identifier)})
  657. @rtype: C{int}
  658. """
  659. return hash(self.identifier)
  660. def __repr__(self) -> str:
  661. """
  662. Returns a string representation of this beacon.
  663. @return: The string representation.
  664. @rtype: C{str}
  665. """
  666. return f"<Beacon ({self.identifier})>"
  667. class Satellite(PositioningBeacon):
  668. """
  669. A satellite.
  670. @ivar azimuth: The azimuth of the satellite. This is the heading (positive
  671. angle relative to true north) where the satellite appears to be to the
  672. device.
  673. @ivar elevation: The (positive) angle above the horizon where this
  674. satellite appears to be to the device.
  675. @ivar signalToNoiseRatio: The signal to noise ratio of the signal coming
  676. from this satellite.
  677. """
  678. def __init__(
  679. self, identifier, azimuth=None, elevation=None, signalToNoiseRatio=None
  680. ):
  681. """
  682. Initializes a satellite object.
  683. @param identifier: The PRN (unique identifier) of this satellite.
  684. @type identifier: C{int}
  685. @param azimuth: The azimuth of the satellite (see instance variable
  686. documentation).
  687. @type azimuth: C{float}
  688. @param elevation: The elevation of the satellite (see instance variable
  689. documentation).
  690. @type elevation: C{float}
  691. @param signalToNoiseRatio: The signal to noise ratio of the connection
  692. to this satellite (see instance variable documentation).
  693. @type signalToNoiseRatio: C{float}
  694. """
  695. PositioningBeacon.__init__(self, int(identifier))
  696. self.azimuth = azimuth
  697. self.elevation = elevation
  698. self.signalToNoiseRatio = signalToNoiseRatio
  699. def __repr__(self) -> str:
  700. """
  701. Returns a string representation of this Satellite.
  702. @return: The string representation.
  703. @rtype: C{str}
  704. """
  705. template = (
  706. "<Satellite ({s.identifier}), "
  707. "azimuth: {s.azimuth}, "
  708. "elevation: {s.elevation}, "
  709. "snr: {s.signalToNoiseRatio}>"
  710. )
  711. return template.format(s=self)
  712. __all__ = [
  713. "Altitude",
  714. "Angle",
  715. "Angles",
  716. "BasePositioningReceiver",
  717. "BeaconInformation",
  718. "Climb",
  719. "Coordinate",
  720. "Directions",
  721. "Heading",
  722. "InvalidChecksum",
  723. "InvalidSentence",
  724. "METERS_PER_FOOT",
  725. "MPS_PER_KNOT",
  726. "MPS_PER_KPH",
  727. "PositionError",
  728. "PositioningBeacon",
  729. "Satellite",
  730. "Speed",
  731. ]