xyz.py 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. # coding: utf-8
  2. import typing
  3. from math import acos
  4. from math import degrees
  5. from math import sqrt
  6. from synergine2.exceptions import SynergineException
  7. from synergine2.simulation import Subject
  8. """
  9. Positions are exprimed as tuple: (x, y, z) Considering start point at top left:
  10. Z
  11. #
  12. #
  13. #
  14. #-------------> X
  15. |.
  16. | .
  17. | .
  18. |
  19. Y
  20. """
  21. COLLECTION_XYZ = 'COLLECTION_XYZ'
  22. NORTH = 11
  23. NORTH_EST = 12
  24. EST = 15
  25. SOUTH_EST = 18
  26. SOUTH = 17
  27. SOUTH_WEST = 16
  28. WEST = 13
  29. NORTH_WEST = 10
  30. DIRECTIONS = (
  31. NORTH,
  32. NORTH_EST,
  33. EST,
  34. SOUTH_EST,
  35. SOUTH,
  36. SOUTH_WEST,
  37. WEST,
  38. NORTH_WEST,
  39. )
  40. DIRECTION_FROM_NORTH_DEGREES = {
  41. (0, 22.5): NORTH,
  42. (22.5, 67): NORTH_EST,
  43. (67, 112.5): EST,
  44. (112.5, 157.5): SOUTH_EST,
  45. (157.5, 202.5): SOUTH,
  46. (202.5, 247.5): SOUTH_WEST,
  47. (247.5, 292.5): WEST,
  48. (292.5, 337.5): NORTH_WEST,
  49. (337.5, 360): NORTH,
  50. (337.5, 0): NORTH
  51. }
  52. DIRECTION_SLIGHTLY = {
  53. NORTH: (NORTH_WEST, NORTH, NORTH_EST),
  54. NORTH_EST: (NORTH, NORTH_EST, EST),
  55. EST: (NORTH_EST, EST, SOUTH_EST),
  56. SOUTH_EST: (EST, SOUTH_EST, SOUTH),
  57. SOUTH: (SOUTH_EST, SOUTH, SOUTH_WEST),
  58. SOUTH_WEST: (SOUTH, SOUTH_WEST, WEST),
  59. WEST: (SOUTH_WEST, WEST, NORTH_WEST),
  60. NORTH_WEST: (WEST, NORTH_WEST, NORTH),
  61. }
  62. DIRECTION_MODIFIERS = {
  63. NORTH_WEST: (-1, -1, 0),
  64. NORTH: (0, -1, 0),
  65. NORTH_EST: (1, -1, 0),
  66. WEST: (-1, 0, 0),
  67. EST: (1, 0, 0),
  68. SOUTH_WEST: (-1, 1, 0),
  69. SOUTH: (0, 1, 0),
  70. SOUTH_EST: (1, 1, 0),
  71. }
  72. class XYZException(SynergineException):
  73. pass
  74. class PositionNotPossible(XYZException):
  75. pass
  76. def get_degree_from_north(a, b):
  77. if a == b:
  78. return 0
  79. ax, ay = a[0], a[1]
  80. bx, by = b[0], b[1]
  81. Dx, Dy = ax, ay - 1
  82. ab = sqrt((bx - ax) ** 2 + (by - ay) ** 2)
  83. aD = sqrt((Dx - ax) ** 2 + (Dy - ay) ** 2)
  84. Db = sqrt((bx - Dx) ** 2 + (by - Dy) ** 2)
  85. degs = degrees(acos((ab ** 2 + aD ** 2 - Db ** 2) / (2 * ab * aD)))
  86. if bx < ax:
  87. return 360 - degs
  88. return degs
  89. class XYZSubjectMixinMetaClass(type):
  90. def __init__(cls, name, parents, attribs):
  91. super().__init__(name, parents, attribs)
  92. collections = getattr(cls, "collections", [])
  93. if COLLECTION_XYZ not in collections:
  94. collections.append(COLLECTION_XYZ)
  95. class XYZSubjectMixin(object, metaclass=XYZSubjectMixinMetaClass):
  96. def __init__(self, *args, **kwargs):
  97. """
  98. :param position: tuple with (x, y, z)
  99. """
  100. self._position = kwargs.pop('position')
  101. self.previous_direction = None
  102. super().__init__(*args, **kwargs)
  103. @property
  104. def position(self):
  105. return self._position
  106. @position.setter
  107. def position(self, value):
  108. self._position = value
  109. class ProximityMixin(object):
  110. distance = 1
  111. feel_collections = [COLLECTION_XYZ]
  112. direction_round_decimals = 0
  113. distance_round_decimals = 2
  114. def have_to_check_position_is_possible(self) -> bool:
  115. return True
  116. def get_for_position(
  117. self,
  118. position,
  119. simulation: 'XYZSimulation',
  120. exclude_subject: Subject=None,
  121. ):
  122. subjects = []
  123. for feel_collection in self.feel_collections:
  124. # TODO: Optimiser en calculant directement les positions alentours et
  125. # en regardant si elles sont occupés dans subjects.xyz par un subject
  126. # etant dans fell_collection
  127. for subject in simulation.collections.get(feel_collection, []):
  128. if subject == exclude_subject:
  129. continue
  130. if self.have_to_check_position_is_possible() and not simulation.is_possible_position(subject.position):
  131. continue
  132. distance = round(
  133. self.get_distance_of(
  134. position=position,
  135. subject=subject,
  136. ),
  137. self.distance_round_decimals,
  138. )
  139. if distance <= self.distance and self.acceptable_subject(subject):
  140. direction = round(
  141. get_degree_from_north(
  142. position,
  143. subject.position,
  144. ),
  145. self.direction_round_decimals,
  146. )
  147. subjects.append({
  148. 'subject': subject,
  149. 'direction': direction,
  150. 'distance': distance,
  151. })
  152. return subjects
  153. @classmethod
  154. def get_distance_of(cls, position, subject: XYZSubjectMixin):
  155. from synergine2_xyz.utils import get_distance_between_points # cyclic import
  156. return get_distance_between_points(
  157. position,
  158. subject.position,
  159. )
  160. def acceptable_subject(self, subject: Subject) -> bool:
  161. return True
  162. def get_direction_from_north_degree(degree: float):
  163. for range, direction in DIRECTION_FROM_NORTH_DEGREES.items():
  164. if range[0] <= degree <= range[1]:
  165. return direction
  166. raise Exception('Degree {} out of range ({})'.format(
  167. degree,
  168. DIRECTION_FROM_NORTH_DEGREES,
  169. ))
  170. def get_neighbor_positions(position: typing.Tuple[int, int]) -> typing.List[typing.Tuple[int, int]]:
  171. neighbors = []
  172. for modifier_x, modifier_y, modifier_z in DIRECTION_MODIFIERS.values():
  173. neighbors.append((position[0] + modifier_x, position[1] + modifier_y))
  174. return neighbors