models.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals, division, print_function
  3. import subprocess
  4. import os
  5. from math import radians, degrees, sin, cos, asin, atan2, sqrt
  6. from django.db import models
  7. from django.conf import settings
  8. from django.core.exceptions import ValidationError
  9. from django.core.validators import MinValueValidator, MaxValueValidator
  10. from django.utils.encoding import python_2_unicode_compatible
  11. EARTH_RADIUS = 6371009
  12. class Point(models.Model):
  13. """Geographical point, with altitude."""
  14. latitude = models.FloatField(verbose_name="latitude", help_text="In degrees",
  15. validators=[MinValueValidator(-90),
  16. MaxValueValidator(90)])
  17. longitude = models.FloatField(verbose_name="longitude", help_text="In degrees",
  18. validators=[MinValueValidator(-180),
  19. MaxValueValidator(180)])
  20. altitude = models.FloatField(verbose_name="altitude", help_text="In meters",
  21. validators=[MinValueValidator(0.)])
  22. @property
  23. def latitude_rad(self):
  24. return radians(self.latitude)
  25. @property
  26. def longitude_rad(self):
  27. return radians(self.longitude)
  28. @property
  29. def altitude_abs(self):
  30. """Absolute distance to the center of Earth (in a spherical model)"""
  31. return EARTH_RADIUS + self.altitude
  32. def line_distance(self, other):
  33. """Distance of the straight line between two points on Earth, in meters.
  34. Note that this is only useful because we are considering
  35. line-of-sight links, where straight-line distance is the relevant
  36. distance. For arbitrary points on Earth, great-circle distance
  37. would most likely be preferred.
  38. """
  39. delta_lon = other.longitude_rad - self.longitude_rad
  40. # Cosine of the angle between the two points on their great circle.
  41. cos_angle = sin(self.latitude_rad) * sin(other.latitude_rad) \
  42. + cos(self.latitude_rad) * cos(other.latitude_rad) * cos(delta_lon)
  43. # Al-Kashi formula
  44. return sqrt(self.altitude_abs ** 2 \
  45. + other.altitude_abs ** 2 \
  46. - 2 * self.altitude_abs * other.altitude_abs * cos_angle)
  47. def bearing(self, other):
  48. """Bearing, in degrees, between this point and another point."""
  49. delta_lon = other.longitude_rad - self.longitude_rad
  50. y = sin(delta_lon) * cos(other.latitude_rad)
  51. x = cos(self.latitude_rad) * sin(other.latitude_rad) \
  52. - sin(self.latitude_rad) * cos(other.latitude_rad) * cos(delta_lon)
  53. return degrees(atan2(y, x))
  54. def elevation(self, other):
  55. """Elevation, in degrees, between this point and another point."""
  56. d = self.line_distance(other)
  57. sin_elev = (other.altitude_abs ** 2 - self.altitude_abs ** 2 - d ** 2) \
  58. / (2 * self.altitude_abs * d)
  59. return degrees(asin(sin_elev))
  60. class Meta:
  61. abstract = True
  62. @python_2_unicode_compatible
  63. class ReferencePoint(Point):
  64. """Reference point, to be used"""
  65. name = models.CharField(verbose_name="name", max_length=255,
  66. help_text="Name of the point")
  67. def to_dict(self):
  68. """Useful to pass information to the javascript code as JSON"""
  69. return {"id": self.id,
  70. "name": self.name,
  71. "latitude": self.latitude,
  72. "longitude": self.longitude,
  73. "altitude": self.altitude}
  74. def to_dict_extended(self, point):
  75. """Same as above, but also includes information relative
  76. to the given point: bearing, azimuth, distance."""
  77. d = self.to_dict()
  78. d['distance'] = self.line_distance(point)
  79. d['bearing'] = point.bearing(self)
  80. d['elevation'] = point.elevation(self)
  81. return d
  82. def __str__(self):
  83. return "Reference point : " + self.name
  84. @python_2_unicode_compatible
  85. class Panorama(ReferencePoint):
  86. loop = models.BooleanField(default=False, verbose_name="360° panorama",
  87. help_text="Whether the panorama loops around the edges")
  88. image = models.ImageField(verbose_name="image", upload_to="pano")
  89. # Set of references, i.e. reference points with information on how
  90. # they relate to this panorama.
  91. references = models.ManyToManyField(ReferencePoint, through='Reference',
  92. related_name="referenced_panorama")
  93. def tiles_dir(self):
  94. return os.path.join(settings.MEDIA_ROOT, settings.PANORAMA_TILES_DIR,
  95. str(self.pk))
  96. def tiles_url(self):
  97. return os.path.join(settings.MEDIA_URL, settings.PANORAMA_TILES_DIR,
  98. str(self.pk))
  99. def generate_tiles(self):
  100. # The trailing slash is necessary for the shell script.
  101. tiles_dir = self.tiles_dir() + "/"
  102. try:
  103. os.makedirs(tiles_dir)
  104. except OSError:
  105. pass
  106. script = os.path.join(settings.BASE_DIR, "panorama", "gen_tiles.sh")
  107. ret = subprocess.call([script, "-p", tiles_dir, self.image.path])
  108. return ret
  109. def __str__(self):
  110. return "Panorama : " + self.name
  111. class Reference(models.Model):
  112. """A reference is made of a Panorama, a Reference Point, and the position
  113. (x, y) of the reference point inside the image. With enough
  114. references, the panorama is calibrated. That is, we can build a
  115. mapping between pixels of the image and directions in 3D space, which
  116. are represented by (azimuth, elevation) couples."""
  117. # Components of the ManyToMany relation
  118. reference_point = models.ForeignKey(ReferencePoint, related_name="refpoint_references")
  119. panorama = models.ForeignKey(Panorama, related_name="panorama_references")
  120. # Position of the reference point in the panorama image
  121. x = models.PositiveIntegerField()
  122. y = models.PositiveIntegerField()
  123. class Meta:
  124. # It makes no sense to have multiple references of the same
  125. # reference point on a given panorama.
  126. unique_together = (("reference_point", "panorama"),)
  127. def clean(self):
  128. # Check that the reference point and the panorama are different
  129. # (remember that panoramas can *also* be seen as reference points)
  130. if self.panorama.pk == self.reference_point.pk:
  131. raise ValidationError("A panorama can't reference itself.")
  132. # Check than the position is within the bounds of the image.
  133. w = self.panorama.image.width
  134. h = self.panorama.image.height
  135. if self.x >= w or self.y >= h:
  136. raise ValidationError("Position ({x}, {y}) is outside the bounds "
  137. "of the image ({width}, {height}).".format(
  138. x=self.x,
  139. y=self.y,
  140. width=w,
  141. height=h))