"""Eve universe models Part 2/2, containing location related models."""
# pylint: disable = too-few-public-methods
import logging
import math
import re
from collections import namedtuple
from http import HTTPStatus
from typing import Iterable, List, Optional, Set
from bitfield import BitField
from django.db import models
from django.utils.functional import cached_property
from esi.exceptions import HTTPClientError
from eveuniverse.constants import EveGroupId, EveRegionId
from eveuniverse.core import dotlan, evesdeapi
from eveuniverse.managers import (
EveAsteroidBeltManager,
EveMoonManager,
EvePlanetManager,
EveStargateManager,
)
from eveuniverse.providers import esi
from .base import EveUniverseEntityModel, _SectionBase, determine_effective_sections
from .entities import EveEntity
from .universe_1 import EveType
logger = logging.getLogger(__name__)
[docs]
class EveAsteroidBelt(EveUniverseEntityModel):
"""An asteroid belt in Eve Online"""
eve_planet = models.ForeignKey(
"EvePlanet", on_delete=models.CASCADE, related_name="eve_asteroid_belts"
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
objects = EveAsteroidBeltManager()
class _EveUniverseMeta:
esi_pk = "asteroid_belt_id"
esi_path_object = "Universe.GetUniverseAsteroidBeltsAsteroidBeltId"
field_mappings = {
"eve_planet": "planet_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
load_order = 200
[docs]
class EveConstellation(EveUniverseEntityModel):
"""A star constellation in Eve Online"""
eve_region = models.ForeignKey(
"EveRegion", on_delete=models.CASCADE, related_name="eve_constellations"
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
class _EveUniverseMeta:
esi_pk = "constellation_id"
esi_path_list = "Universe.GetUniverseConstellations"
esi_path_object = "Universe.GetUniverseConstellationsConstellationId"
field_mappings = {
"eve_region": "region_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
children = {"systems": "EveSolarSystem"}
load_order = 192
[docs]
@classmethod
def eve_entity_category(cls) -> str:
return EveEntity.CATEGORY_CONSTELLATION
[docs]
class EveMoon(EveUniverseEntityModel):
"""A moon in Eve Online"""
eve_planet = models.ForeignKey(
"EvePlanet", on_delete=models.CASCADE, related_name="eve_moons"
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
objects = EveMoonManager()
class _EveUniverseMeta:
esi_pk = "moon_id"
esi_path_object = "Universe.GetUniverseMoonsMoonId"
field_mappings = {
"eve_planet": "planet_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
load_order = 220
[docs]
class EvePlanet(EveUniverseEntityModel):
"""A planet in Eve Online"""
[docs]
class Section(_SectionBase):
"""Sections that can be optionally loaded with each instance"""
ASTEROID_BELTS = "asteroid_belts" #:
MOONS = "moons" #:
eve_solar_system = models.ForeignKey(
"EveSolarSystem", on_delete=models.CASCADE, related_name="eve_planets"
)
eve_type = models.ForeignKey(
"EveType", on_delete=models.CASCADE, related_name="eve_planets"
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
enabled_sections = BitField(
flags=tuple(Section.values()),
help_text=(
"Flags for loadable sections. True if instance was loaded with section."
), # no index, because MySQL does not support it for bitwise operations
) # type: ignore
objects = EvePlanetManager()
class _EveUniverseMeta:
esi_pk = "planet_id"
esi_path_object = "Universe.GetUniversePlanetsPlanetId"
field_mappings = {
"eve_solar_system": "system_id",
"eve_type": "type_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
children = {"moons": "EveMoon", "asteroid_belts": "EveAsteroidBelt"}
load_order = 205
[docs]
def type_name(self) -> str:
"""Return shortened name of planet type.
Note: Accesses the eve_type object.
"""
matches = re.findall(r"Planet \((\S*)\)", self.eve_type.name)
return matches[0] if matches else ""
@classmethod
def _children(cls, enabled_sections: Optional[Set[str]] = None) -> dict:
enabled_sections = determine_effective_sections(enabled_sections)
children = {}
if cls.Section.ASTEROID_BELTS in enabled_sections:
children["asteroid_belts"] = "EveAsteroidBelt"
if cls.Section.MOONS in enabled_sections:
children["moons"] = "EveMoon"
return children
[docs]
class EveRegion(EveUniverseEntityModel):
"""A star region in Eve Online"""
description = models.TextField(default="")
class _EveUniverseMeta:
esi_pk = "region_id"
esi_path_list = "Universe.GetUniverseRegions"
esi_path_object = "Universe.GetUniverseRegionsRegionId"
children = {"constellations": "EveConstellation"}
load_order = 190
@property
def profile_url(self) -> str:
"""URL to default third party website with profile info about this entity."""
return dotlan.region_url(self.name)
[docs]
@classmethod
def eve_entity_category(cls) -> str:
return EveEntity.CATEGORY_REGION
[docs]
class EveSolarSystem(EveUniverseEntityModel):
"""A solar system in Eve Online"""
[docs]
class Section(_SectionBase):
"""Sections that can be optionally loaded with each instance"""
PLANETS = "planets" #:
STARGATES = "stargates" #:
STARS = "stars" #:
STATIONS = "stations" #:
eve_constellation = models.ForeignKey(
"EveConstellation", on_delete=models.CASCADE, related_name="eve_solarsystems"
)
eve_star = models.OneToOneField(
"EveStar",
on_delete=models.SET_DEFAULT,
default=None,
null=True,
related_name="eve_solarsystem",
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
security_status = models.FloatField()
enabled_sections = BitField(
flags=tuple(Section.values()),
help_text=(
"Flags for loadable sections. True if instance was loaded with section."
), # no index, because MySQL does not support it for bitwise operations
) # type: ignore
class _EveUniverseMeta:
esi_pk = "system_id"
esi_path_list = "Universe.GetUniverseSystems"
esi_path_object = "Universe.GetUniverseSystemsSystemId"
field_mappings = {
"eve_constellation": "constellation_id",
"eve_star": "star_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
children = {}
load_order = 194
NearestCelestial = namedtuple(
"NearestCelestial", ["eve_type", "eve_object", "distance"]
)
NearestCelestial.__doc__ = "Container for a nearest celestial"
@property
def profile_url(self) -> str:
"""Return URL to default third party website
with profile info about this solar system.
"""
return dotlan.solar_system_url(self.name)
@property
def is_high_sec(self) -> bool:
"""Return True when this solar system is in high sec, else False."""
return self.security_status >= 0.45
@property
def is_low_sec(self) -> bool:
"""Return True when this solar system is in low sec, else False."""
return 0.0 < self.security_status < 0.45
@property
def is_null_sec(self) -> bool:
"""Return True when this solar system is in null sec, else False."""
return (
not self.is_w_space
and not self.is_trig_space
and not self.is_abyssal_deadspace
and self.security_status <= 0.0
and not self.is_w_space
)
@property
def is_w_space(self) -> bool:
"""Return True when this solar system is in wormhole space, else False."""
return 31_000_000 <= self.id < 32_000_000
@cached_property
def is_trig_space(self) -> bool:
"""Return True when this solar system is in Triglavian space, else False."""
return self.eve_constellation.eve_region_id == EveRegionId.POCHVEN
@property
def is_abyssal_deadspace(self) -> bool:
"""Return True when this solar system is in abyssal deadspace, else False."""
return 32_000_000 <= self.id < 33_000_000
[docs]
@classmethod
def eve_entity_category(cls) -> str:
"""Return related EveEntity category."""
return EveEntity.CATEGORY_SOLAR_SYSTEM
[docs]
def distance_to(self, destination: "EveSolarSystem") -> Optional[float]:
"""Calculates the distance in meters between the current and the given solar system
Args:
destination: Other solar system to use in calculation
Returns:
Distance in meters or None if one of the systems is in WH space
"""
if not self.position_x or not self.position_y or not self.position_z:
return None
if (
not destination
or not destination.position_x
or not destination.position_y
or not destination.position_z
):
return None
if (
self.is_w_space
or destination.is_w_space
or self.is_trig_space
or destination.is_trig_space
):
return None
return math.sqrt(
(destination.position_x - self.position_x) ** 2
+ (destination.position_y - self.position_y) ** 2
+ (destination.position_z - self.position_z) ** 2
)
[docs]
def route_to(
self, destination: "EveSolarSystem"
) -> Optional[List["EveSolarSystem"]]:
"""Calculates the shortest route between the current and the given solar system
Args:
destination: Other solar system to use in calculation
Returns:
List of solar system objects incl. origin and destination
or None if no route can be found (e.g. if one system is in WH space)
"""
if (
self.is_w_space
or destination.is_w_space
or self.is_trig_space
or destination.is_trig_space
):
return None
path_ids = self._calc_route_esi(self.id, destination.id)
if path_ids is None:
return None
return [
EveSolarSystem.objects.get_or_create_esi(id=solar_system_id) # type: ignore
for solar_system_id in path_ids
]
[docs]
def jumps_to(self, destination: "EveSolarSystem") -> Optional[int]:
"""Calculates the shortest route between the current and the given solar system
Args:
destination: Other solar system to use in calculation
Returns:
Number of total jumps
or None if no route can be found (e.g. if one system is in WH space)
"""
if (
self.is_w_space
or destination.is_w_space
or self.is_trig_space
or destination.is_trig_space
):
return None
path_ids = self._calc_route_esi(self.id, destination.id)
if not path_ids:
return None
return len(path_ids) - 1
@staticmethod
def _calc_route_esi(origin_id: int, destination_id: int) -> Optional[List[int]]:
"""returns the shortest route between two given solar systems.
Route is calculated by ESI
Args:
destination_id: ID of the other solar system to use in calculation
Returns:
List of solar system IDs incl. origin and destination
or None if no route can be found (e.g. if one system is in WH space)
"""
try:
response = esi.client.Routes.PostRoute(
body={},
origin_system_id=origin_id,
destination_system_id=destination_id,
).result(use_etag=False)
return response.route
except HTTPClientError as ex:
if ex.status_code == HTTPStatus.NOT_FOUND:
return None # no route found
raise ex
[docs]
def nearest_celestial(
self, x: int, y: int, z: int, group_id: Optional[int] = None
) -> Optional[NearestCelestial]:
"""Determine nearest celestial to given coordinates as eveuniverse object.
Args:
x, y, z: Start point in space to look from
group_id: Eve ID of group to filter results by
Raises:
HTTPError: If an HTTP error is encountered
ValueError: If there is an semantic issue
Returns:
Eve item or None if none is found
"""
item = evesdeapi.nearest_celestial(
solar_system_id=self.id, x=x, y=y, z=z, group_id=group_id
)
if not item:
return None
eve_type, _ = EveType.objects.get_or_create_esi(id=item.type_id) # type: ignore
class_mapping = {
EveGroupId.ASTEROID_BELT: EveAsteroidBelt,
EveGroupId.MOON: EveMoon,
EveGroupId.PLANET: EvePlanet,
EveGroupId.STAR: EveStar,
EveGroupId.STARGATE: EveStargate,
EveGroupId.STATION: EveStation,
}
try:
my_class = class_mapping[eve_type.eve_group_id]
except KeyError:
logger.debug(
"Nearest celestial returned from API has unexpected type ID: %d",
eve_type.id,
)
return None
obj, _ = my_class.objects.get_or_create_esi(id=item.id)
result = self.NearestCelestial(
eve_type=eve_type, eve_object=obj, distance=item.distance
)
return result
@classmethod
def _children(cls, enabled_sections: Optional[Set[str]] = None) -> dict:
enabled_sections = determine_effective_sections(enabled_sections)
children = {}
if cls.Section.PLANETS in enabled_sections:
children["planets"] = "EvePlanet"
if cls.Section.STARGATES in enabled_sections:
children["stargates"] = "EveStargate"
if cls.Section.STATIONS in enabled_sections:
children["stations"] = "EveStation"
return children
@classmethod
def _disabled_fields(cls, enabled_sections: Optional[Set[str]] = None) -> set:
enabled_sections = determine_effective_sections(enabled_sections)
if cls.Section.STARS not in enabled_sections:
return {"eve_star"}
return set()
@classmethod
def _inline_objects(cls, enabled_sections: Optional[Set[str]] = None) -> dict:
if not enabled_sections or cls.Section.PLANETS not in enabled_sections:
return {}
return super()._inline_objects()
[docs]
class EveStar(EveUniverseEntityModel):
"""A star in Eve Online"""
age = models.BigIntegerField()
eve_type = models.ForeignKey(
"EveType", on_delete=models.CASCADE, related_name="eve_stars"
)
luminosity = models.FloatField()
radius = models.PositiveIntegerField()
spectral_class = models.CharField(max_length=16)
temperature = models.PositiveIntegerField()
class _EveUniverseMeta:
esi_pk = "star_id"
esi_path_object = "Universe.GetUniverseStarsStarId"
field_mappings = {"eve_type": "type_id"}
load_order = 222
[docs]
class EveStargate(EveUniverseEntityModel):
"""A stargate in Eve Online"""
destination_eve_stargate = models.OneToOneField(
"EveStargate", on_delete=models.SET_DEFAULT, null=True, default=None, blank=True
)
destination_eve_solar_system = models.ForeignKey(
"EveSolarSystem",
on_delete=models.SET_DEFAULT,
null=True,
default=None,
blank=True,
related_name="destination_eve_stargates",
)
eve_solar_system = models.ForeignKey(
"EveSolarSystem", on_delete=models.CASCADE, related_name="eve_stargates"
)
eve_type = models.ForeignKey(
"EveType", on_delete=models.CASCADE, related_name="eve_stargates"
)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
objects = EveStargateManager()
class _EveUniverseMeta:
esi_pk = "stargate_id"
esi_path_object = "Universe.GetUniverseStargatesStargateId"
field_mappings = {
"destination_eve_stargate": ("destination", "stargate_id"),
"destination_eve_solar_system": ("destination", "system_id"),
"eve_solar_system": "system_id",
"eve_type": "type_id",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
dont_create_related = {
"destination_eve_stargate",
"destination_eve_solar_system",
}
load_order = 224
[docs]
class EveStation(EveUniverseEntityModel):
"""A space station in Eve Online"""
eve_race = models.ForeignKey(
"EveRace",
on_delete=models.SET_DEFAULT,
default=None,
null=True,
related_name="eve_stations",
)
eve_solar_system = models.ForeignKey(
"EveSolarSystem",
on_delete=models.CASCADE,
related_name="eve_stations",
)
eve_type = models.ForeignKey(
"EveType",
on_delete=models.CASCADE,
related_name="eve_stations",
)
max_dockable_ship_volume = models.FloatField()
office_rental_cost = models.FloatField()
owner_id = models.PositiveIntegerField(default=None, null=True, db_index=True)
position_x = models.FloatField(
null=True, default=None, blank=True, help_text="x position in the solar system"
)
position_y = models.FloatField(
null=True, default=None, blank=True, help_text="y position in the solar system"
)
position_z = models.FloatField(
null=True, default=None, blank=True, help_text="z position in the solar system"
)
reprocessing_efficiency = models.FloatField()
reprocessing_stations_take = models.FloatField()
services = models.ManyToManyField("EveStationService")
class _EveUniverseMeta:
esi_pk = "station_id"
esi_path_object = "Universe.GetUniverseStationsStationId"
field_mappings = {
"eve_race": "race_id",
"eve_solar_system": "system_id",
"eve_type": "type_id",
"owner_id": "owner",
"position_x": ("position", "x"),
"position_y": ("position", "y"),
"position_z": ("position", "z"),
}
inline_objects = {"services": "EveStationService"}
load_order = 207
[docs]
@classmethod
def eve_entity_category(cls) -> str:
return EveEntity.CATEGORY_STATION
@classmethod
def _update_or_create_inline_objects(
cls,
*,
parent_eve_data_obj: dict,
parent_obj,
wait_for_children: bool,
enabled_sections: Iterable[str],
task_priority: Optional[int] = None,
) -> None:
"""updates_or_creates station service objects for EveStations"""
if "services" in parent_eve_data_obj:
services = []
for service_name in parent_eve_data_obj["services"]:
service, _ = EveStationService.objects.get_or_create(name=service_name)
services.append(service)
if services:
parent_obj.services.add(*services)
[docs]
class EveStationService(models.Model):
"""A service in a space station"""
name = models.CharField(max_length=50, unique=True)
def __str__(self) -> str:
return self.name